Why a connector #
You are in the car, or on the sofa watching a film and you don't want to go and find a keyboard. Your phone is there. Your brain isn't. There were already two ways to reach it from a phone and both ask for something you won't do at that moment.
You can drive a Claude Code session from the app today. But that is one project per session and somebody has to have left the session open. The web UI over your tailnet is fine for looking something up, but it can't save anything you tell it. Neither lets an ordinary chat on your phone know what this project decided last month, or save what you just decided in the passenger seat.
A custom connector does. The brain's MCP server already works over HTTP. All that is left is to put it somewhere the app can reach and handle the sign-in the app asks for before it will talk to anything. After that every chat on your phone has the brain's tools. No session open anywhere, no project picked in advance and nothing running on the phone.
What the app needs and why a token is not enough #
A remote connector can't use a token you pasted into a config file. That works for stdio and for a private network and the MCP page covers it. The Claude app follows the MCP sign-in spec (OAuth). It expects four things. A 401 reply that says where to find out who can sign you in. Discovery documents at well-known URLs. A way to register itself, so there is no client id for you to paste. HTTPS on every address involved. Miss any of these and the app gives no clear error. It just won't connect.
So the sign-in server is built into the binary. One flag turns it on and it is off unless you give that flag:
$ ns-brain mcp --http 127.0.0.1:7801 \
--public-url https://yourmachine.your-tailnet.ts.net \
--oauth-redirect-host claude.ai \
--token "$TOKEN"
The password on the login page is that token. It is the only secret in the whole setup, so treat it like one: it guards every memory the machine can reach and the tools that can change them. It works only as a password. The brain's endpoint won't accept it as a token, so the login page is the only place anyone can try it. Each wrong guess makes the address it came from wait longer, so guessing is pointless, while you can still get in from any other address. Every refusal and every success is logged and the password never appears in the log.
The login page also shows the site your access is about to be sent to. Any app can register under any name, so read that line before you type. If it isn't the app you are connecting, stop. Better still, don't rely on reading it at one in the morning: --oauth-redirect-host claude.ai lets only the Claude app register, so a link that would send your login anywhere else is refused before any page appears. Repeat the flag for each app you use. What the app gets after you sign in expires quickly and renews itself. Only a hash of it is kept, with your other credentials, readable only by you (mode 600), so restarting the server doesn't sign your phone out.
The flag comes with two refusals. Better to meet them here than at one in the morning. --public-url won't take a plain http URL, because a sign-in server over plain http hands your credentials to anyone on the network path. It won't run without a token either, because the token is the login page's password and a login page with no password is a door anyone can open.
Giving it an address #
The server listens only on your own machine (loopback). Tailscale holds the certificate for your machine's name on your tailnet, handles the HTTPS and passes requests on to that local port. So you have no reverse proxy to run and no certificate to renew.
$ tailscale funnel --bg --https=443 7801 https://yourmachine.your-tailnet.ts.net -> 127.0.0.1:7801
You turn Funnel on once for the tailnet. The CLI prints an admin link and waits while you do it. Switch on MagicDNS and HTTPS certificates in the same console. The certificate for the name is fetched the first time it is used, which takes around a minute. During that minute HTTPS requests hang instead of failing, so wait before you decide something is wrong.
The connector has to be on 443. Give the app a URL with a port in it and it says the connector failed to start. It never sends your server a request, so your log is empty and nothing on the phone tells you the port was the problem. Anything else you publish from that machine needs another port.
Which brain the phone sees #
The server uses the brain of the folder it runs in. Run it from a project that uses the shared store and it serves every project in that store. Each call names one with _project. Call a tool without naming a project and it replies with the list of projects it has, so the model can ask again. A project with its own file is not in that store. It answers with no memories, which looks like an empty brain but isn't.
Where a save goes is decided the same way. Get this right before you rely on it. Run the connector on the machine that holds the brain. Then what you dictate on the phone is in the brain your laptop reads tomorrow. Run it in a folder whose memories really live somewhere else and you have saved 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. That is the same endpoint the CLI uses and the root serves nothing. Leave client id and secret empty. Connect opens the login page. Type the token and allow access. The chat now has the brain's tools, the same list the MCP server gives any other client.
If you have a second machine, add a second connector for it. The app keeps both. The next section says why that is worth it.
What it costs and what has to be true #
Nothing to buy. No proxy to run in front of it, no OAuth provider to sign up with, no gateway, no tunnel service and no account with us. The binary you already installed serves the endpoint and the sign-in.
What it does need is a short list. Tailscale on the machine, with Funnel turned on once for the tailnet. Something that restarts the process after a reboot: a launchd plist or a systemd unit, about ten lines. A password you treat as a password.
And the machine has to be awake. That is the real limit and no engineering gets around it: a phone calling a desktop that is asleep gets nothing. If your brain is on a desktop that stays on, you are done. If it is on a laptop you close, set the same thing up on both and add two connectors in the app, so whichever is awake 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 gives up before it sends a request, so an empty log is the clue. Publish on 443. |
| The first HTTPS request hangs | The certificate for the name is being fetched. It takes around a minute the first time and never again. |
| 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 on purpose. |
| "this server fronts a shared brain" | It is working. Name the project with _project, or set it in the server's own config. The three ways, best first. |
| A project you know has memories answers with none | Right server, wrong store. A server on the shared store can't see a project that has its own file. |