Why a connector #
You are in the car, or at home watching a film and unwilling to go and find a keyboard. The phone is there. The brain is not and the two ways of reaching it from a phone that already worked both cost something you will not pay at that moment.
A Claude Code session driven from the app works today. It is one project per session and it needs a session somebody left open. The web UI over your tailnet is fine for looking something up and then it forgets you were there. Neither one lets an ordinary chat on the phone know what this project decided last month, or write down what you have just decided in the passenger seat.
A custom connector does. The brain's own MCP server already speaks HTTP over the network, so the work is publishing it somewhere the app can reach and answering the authorization the app demands before it will talk to anything. After that every chat on the phone has the brain's tools in it. No session open anywhere, no project chosen in advance, nothing running on the phone at all.
What the app requires and why a token is not enough #
A remote connector does not carry a bearer token you pasted into a config file. That is the stdio and the private-network case and the MCP page covers it. The Claude app follows the MCP authorization spec, which means it expects a 401 that names where to go and find out who can authorize you, discovery documents at well-known URLs, a registration it performs itself so there is no client id for you to paste and HTTPS on every endpoint involved. Miss any of it and the app does not fail loudly. It declines to connect.
So the authorization server is in the binary. One flag mounts it and it is mounted only when that flag is given:
$ ns-brain mcp --http 127.0.0.1:7801 \
--public-url https://yourmachine.your-tailnet.ts.net \
--token "$TOKEN"
The password on the login page is that token. It is the only credential in the design, which is the argument for treating it like one: it stands in front of every memory the machine running it can reach and in front of the tools that can change them. Wrong answers lock the page for long enough to make guessing pointless and every refusal and every success is logged without the password ever appearing in the log. What the app is issued afterwards is short-lived and refreshes itself and it is kept with your other credentials at mode 600, so restarting the server does not knock the phone off. Two refusals come with the flag and both are worth meeting on this page rather than at one in the morning. --public-url will not take a plain http URL, because an authorization server reached over plain http is a credential handed to whoever is on the path. It will not run without a token either, since that token is the password on the login page and a login page with no password is a door with a handle on it.
Putting a name on it #
The server binds loopback and stays there. Tailscale holds the certificate for your machine's name on your tailnet, terminates TLS and forwards to that loopback port, which is why there is no reverse proxy of your own to run here and no certificate of your own to renew.
$ tailscale funnel --bg --https=443 7801 https://yourmachine.your-tailnet.ts.net -> 127.0.0.1:7801
Funnel has to be turned on once for the tailnet. The CLI prints an admin link and waits while you do it. MagicDNS and HTTPS certificates both go on in the same console and the certificate for the name is fetched on first use, which takes around a minute. During that minute HTTPS requests hang rather than fail, so wait it out before you conclude anything is wrong.
The connector has to be on 443. Given a URL with a port in it the app reports that the connector failed to start, without ever making a request to your server, so there is nothing in your log to read and no way to tell from the phone that the port is what it objected to. Anything else you publish from that machine takes another port.
Which brain the phone sees #
The server serves the brain of the directory it runs in. Run it from a project whose store is the shared one and it fronts every project in that store, each call naming one through _project. Call a tool without naming a project and it answers with the list of projects it holds, which is enough for the model to ask again. A project sitting in its own file is not in that store and answers with no memories, which reads like an empty brain and is not one.
Where a write lands is decided the same way and it is the part worth getting right before you rely on it. Run the connector on the machine that holds the brain, or on a machine enrolled against a Hive server and what you dictate on the phone is on the brain your laptop reads tomorrow. Run it in a directory whose memories actually live somewhere else and you have written to a copy.
Adding it in the app #
Settings, then Connectors, then Add custom connector. The URL is your tailnet name with /mcp on the end, the same endpoint the CLI uses, because the root serves nothing. Leave client id and secret empty. Connect opens the login page, you type the token and allow it and the chat has the brain's tools: 58 of them in the current build.
Add a second connector pointed at a second machine if you have one. The app holds both and the reason to bother is in the next section.
What it costs and what has to be true #
Nothing to buy. This is the free edition, like the rest of MCP and the paid line is drawn on a second person rather than a second machine or a second device. There is no proxy to run in front of it, no OAuth provider to register with, no gateway, no tunnel service and no account with us. The binary you already installed serves the endpoint and the authorization for it.
What it does need is honest and short. Tailscale on the machine and Funnel enabled once for the tailnet. Something keeping the process up across a reboot, which is a launchd plist or a systemd unit and about ten lines of it. A password you treat as a password.
And the machine has to be awake. That is the real constraint and it is not one any amount of engineering removes: a phone reaching a desktop that is asleep reaches nothing. If the machine that holds your brain is a desktop that stays on, you are done. If it is a laptop that closes, stand the same thing up on both and let the app hold two connectors, so the one that is up answers.
When it does not work #
| What you see | What it is |
|---|---|
| "failed to start" in the app, nothing at all in your log | A port in the URL. The app refuses before it makes a request, so silence on your side is the symptom. Publish on 443. |
| The first HTTPS request hangs | The certificate for the name is being fetched. Around a minute the first time and never again after that. |
| 404 on every request | The endpoint is /mcp. The root serves nothing. |
Refuses to start, naming --public-url | Either a plain http URL or no token. Both are refused deliberately. |
| "this server fronts a shared brain" | Working. Name the project with _project, or set it in the server's own config. The three ways, in preference order. |
| A project you know has memories answers with none | Right server, wrong store. A project on its own file is invisible to a server fronting the shared one. |