Remote MCP access (tunnels for cloud agents)¶
Use this when a cloud agent (Cursor Cloud, Codex cloud, Claude remote, and similar) cannot reach your Hub over Tailscale or LAN, but you still want live MCP instead of the forge issue mailbox.
Use the least-public path that works:
- Private mesh when the agent can join it.
- Controlled HTTPS tunnel when it cannot.
- Forge mailbox when durable live MCP is not practical.
Never solve reachability by exposing anonymous or unauthenticated /mcp.
Choose a path¶
| Preference | Path | When |
|---|---|---|
| 1 — Primary | Cloudflare Tunnel (or another operator-controlled HTTPS tunnel) | Cloud agent cannot join your tailnet; you need a public HTTPS hostname you control |
| 2 — Secondary | Tailscale mesh | Windows / Dev LXC / cloud agent can reach the Hub on the private tailnet |
| 3 — Caution | ngrok / ephemeral tunnels | Short-lived demos only; rotate token and expect hostname churn |
Always-on reverse proxies (Caddy, nginx, Tailscale Serve) are covered in Installation and Homelab deploy. This page is specifically about exposing Hub MCP to remote/cloud agents.
Security checklist (required)¶
Before exposing MCP beyond loopback, verify every item below:
- Strong
ADHD_HUB_AUTH_TOKEN— long random secret; never leavechange-me, empty, or the.env.exampleplaceholder when the Hub is reachable beyond loopback. The server refuses to start with a weak token if you also set a non-loopback bind, a non-loopbackADHD_HUB_PUBLIC_URL, orADHD_HUB_TRUST_PROXY_HEADERS. - No anonymous
/mcp— clients must sendAuthorization: Bearer <token>(or complete Hub OAuth). Dashboard cookies do not authorize MCP. - HTTPS +
ADHD_HUB_PUBLIC_URL— set the public base to the tunnel hostname (https://…, no trailing slash). OAuth discovery and install links must match that hostname. - TLS at the tunnel terminator — do not publish Hub’s plain HTTP port on the open internet. Terminate TLS at Cloudflare Tunnel / Serve / your proxy; with Compose + tunnel, leave Hub on the internal Docker network only (comment out host
8787publish). ADHD_HUB_TRUST_PROXY_HEADERS=trueonly behind a trusted terminator — never enable proxy trust on a Hub that faces the internet directly.- Secrets stay out of git — tunnel tokens and
cloudflaredcredential files belong in.env/secrets/(gitignored), never in compose committed uncommented with real values. - Optional Cloudflare Access — can gate the public hostname for humans; MCP clients still need Hub Bearer/OAuth.
- Forge mailbox fallback — if you cannot keep a durable private or tunnel path, use forge issue inbox rather than opening an unauthenticated endpoint.
- Rotate after ephemeral URLs — if the public hostname changes (ngrok free tier, Funnel demos), update client MCP config and consider rotating the bearer token.
Out of scope here: OpenClaw SSH tunnels used only for OAuth callbacks (see OpenClaw MCP OAuth).
Cloudflare Tunnel (primary)¶
Pattern: Hub listens on a private Docker network (or loopback). cloudflared terminates HTTPS and forwards to Hub. Prefer the commented opt-in service in the repository docker-compose.yml over publishing host 8787 to the open internet.
Compose opt-in (recommended)¶
- Copy
.env.example→.envand set a strongADHD_HUB_AUTH_TOKEN(never leavechange-me/ the example placeholder). - In
docker-compose.yml, comment out the Hubports:mapping so8787is not published on the host. - Uncomment the
cloudflaredservice. PutCLOUDFLARE_TUNNEL_TOKENin.env(or mount./secrets/cloudflaredread-only — that directory is gitignored). - In Cloudflare Zero Trust, route
https://hub.example.com→http://adhd-hub:8787(Docker DNS name, not host127.0.0.1). - On the Hub:
ADHD_HUB_AUTH_TOKEN=<long-random-secret>
ADHD_HUB_PUBLIC_URL=https://hub.example.com
ADHD_HUB_TRUST_PROXY_HEADERS=true
- Confirm health and MCP over HTTPS:
curl -fsS "$ADHD_HUB_PUBLIC_URL/api/health"
curl -fsS -H "Authorization: Bearer $ADHD_HUB_AUTH_TOKEN" \
-H "Accept: application/json, text/event-stream" \
"$ADHD_HUB_PUBLIC_URL/mcp"
- Point cloud MCP clients at
https://hub.example.com/mcpwith the same bearer token (see Cursor Cloud wiring).
Optional: put Cloudflare Access in front of the tunnel hostname for an extra operator login gate. Access does not replace Hub Bearer/OAuth for MCP clients — agents still need Authorization: Bearer (or Hub OAuth).
Hub refuse-to-start: a weak/default token combined with non-loopback bind, non-loopback ADHD_HUB_PUBLIC_URL, or ADHD_HUB_TRUST_PROXY_HEADERS=true fails closed at startup. Pure local loopback + default token without those signals remains allowed for intentional local-dev.
Tailscale Funnel can play a similar role (HTTPS hostname → local Hub). Treat Funnel like any other public terminator: strong token, ADHD_HUB_PUBLIC_URL, TLS at the edge, proxy trust only when Funnel/Serve is the trusted front.
Tailscale mesh (secondary)¶
When Cursor on a PC, a Dev LXC, or a cloud agent can join the same tailnet, keep Hub private:
- Publish
:8787on the Tailscale interface only (homelab Compose pattern). - MCP URL:
http://<tailscale-host-or-magicdns>:8787/mcp(plain HTTP on Tailscale is acceptable; HTTPS still preferred if you terminate with Serve). - Still require Bearer (or OAuth). Do not disable auth because the path is “private.”
Canonical layout: Homelab deployment. If the cloud runner cannot install Tailscale or reach the tailnet, use Cloudflare Tunnel or the forge mailbox — do not assume mesh reachability from Cursor Cloud.
ngrok and similar (cautionary)¶
Ephemeral public URLs are easy to misconfigure:
- Hostname changes break
ADHD_HUB_PUBLIC_URL, OAuth discovery, and saved MCP configs. - Free/shared tunnels are easier to guess or leave running after a demo.
- Logging side channels (request logs, shared dashboards) may capture
Authorizationheaders if you are careless.
If you must use ngrok (or similar) briefly:
- Put a strong bearer token in place before the tunnel is up.
- Set
ADHD_HUB_PUBLIC_URLto the currenthttps://…hostname every time it changes. - Enable
ADHD_HUB_TRUST_PROXY_HEADERSonly if that tool is your trusted TLS terminator. - Tear the tunnel down when finished; rotate
ADHD_HUB_AUTH_TOKENafterward. - Prefer forge mailbox for ongoing cloud work if you cannot maintain a stable tunnel.
Cursor Cloud / cloud-agent wiring¶
Cloud agents need the Streamable HTTP MCP URL and a secret they can read from env or the product’s MCP secret store — not a pasted token in a chat transcript.
Typical values:
| Setting | Value |
|---|---|
| MCP URL | https://<your-tunnel-host>/mcp (or Tailscale URL if reachable) |
| Auth | Authorization: Bearer <ADHD_HUB_AUTH_TOKEN> |
| Env helpers | ADHD_HUB_MCP_URL / ADHD_HUB_AUTH_TOKEN (Marketplace plugin and adapters) |
Practical notes:
- In Cursor, configure the cloud/environment MCP server to the tunnel HTTPS URL and inject the token from secrets / environment — same shape as adapters/cursor-mcp.json (
url+${env:ADHD_HUB_AUTH_TOKEN}). - Marketplace plugin installs often expect
ADHD_HUB_MCP_URL({base}/mcp) andADHD_HUB_AUTH_TOKEN; keep those aligned withADHD_HUB_PUBLIC_URL. - If MCP tools are missing, errored, or unauthorized after tunnel changes, fix URL/token/HTTPS first. Until MCP works, agents should use the forge issue inbox and must not invent Hub continuity.
- Skip or cancel hanging Auth / Authenticate prompts until Hub OAuth is intentionally enabled for that public URL; static Bearer remains supported.
See also Connect for local/CLI connect and Authentication for dashboard vs MCP auth.
Related¶
- Repository
docker-compose.yml— commentedcloudflaredopt-in - Forge issue inbox — fallback when live MCP is unreachable
- Homelab deployment — Proxmox + Tailscale
- Installation — reverse proxy / HTTPS
- Environment variables
- SECURITY.md