Deploy GBrain Remote MCP Server
September 21, 2026 · View on GitHub
gbrain serve --httpships full OAuth 2.1 (client credentials, auth code + PKCE, refresh rotation, optional DCR), an embedded React admin dashboard at/admin, scoped operations, and a live SSE activity feed. Legacy bearer tokens also work:verifyAccessTokenfalls back to theaccess_tokenstable; tokens with noscopesgrant carryread+write+admin, while tokens minted withgbrain auth create --scopes …(or bygbrain bootstrap harness) are honored at exactly their granted scopes. Both the legacy fallback and the OAuth tables work on PGLite and Postgres (both engine schemas carryaccess_tokens). See SECURITY.md for env vars and tunable defaults.
Access your brain from any device, any AI client. GBrain ships two transports:
gbrain serve (stdio) for local agents, and gbrain serve --http for remote
clients over OAuth 2.1. The recommended way to publish serve --http from your
own computer is gbrain mcp expose, which puts it on your Tailscale tailnet
with HTTPS and keeps it running as a user service — see
Use your brain from anywhere over MCP.
Say to your agent: "use my brain over mcp" — "put my brain on
tailscale" — the remote-mcp skill runs gbrain mcp expose for you after
you confirm the printed plan.
Authorization-code connections require owner approval in the admin dashboard. Existing sessions are preserved. Before upgrading an installation with queued work, follow the authorization and worker upgrade guide for the coordinated cutover, consent recovery, and Bun requirements.
The owner-approval step. /authorize never returns an authorization code
on its own. It records a pending request and redirects the browser to the admin
dashboard (/admin/?oauth_request=…), where the brain owner signs in (bootstrap
token or magic link), reviews the client name, redirect URI and requested
scopes, and approves or denies. Only an approval mints the code, which is then
delivered to the client's registered redirect URI; a denial returns
error=access_denied. This applies to every authorization-code client,
including clients that self-registered via DCR — self-registration alone never
yields a token. Pending requests expire after ten minutes, do not survive a
server restart, and are bounded: at most ten awaiting-decision requests per
client and a fixed server-wide ceiling. Beyond either, /authorize sends the
client back to its registered redirect URI with error=too_many_requests
(and no code) until earlier requests are decided or expire; a 429 status on
/authorize comes only from the MCP SDK's per-IP rate limit.
Say to your agent: "Start the brain server over HTTP with self-service
registration, then approve my client in the admin dashboard — your agent runs
gbrain serve --http --enable-dcr and you finish the connection by approving
it at /admin/."
Three Paths
Local stdio (zero setup)
gbrain serve # full operation catalog (default)
gbrain serve --surface verbs # just the 7 memory verbs (quickstart surface)
Works with Claude Code, Cursor, Windsurf, and any MCP client that supports stdio.
No server, no tunnel, no token needed. Works on both PGLite and Postgres engines.
--surface verbs exposes exactly the seven-verb memory protocol (recall,
remember, entity, synthesize, forget, context_pack, delta —
MEMORY_VERBS v1) instead of the full catalog;
--surface starter sits between (~27 ops: the verbs plus the daily-driver set);
omit the flag (default full) for every operation.
Tailscale (recommended): gbrain mcp expose
gbrain mcp expose # tailnet-only HTTPS: your own devices
gbrain mcp expose --funnel # public HTTPS on the same name: cloud agents (Grok Bot, Muse, ChatGPT, …)
gbrain mcp expose --status # re-probe the receipt, service and health
One command on the brain host: installs Tailscale if needed (after a consent
prompt), signs in, publishes gbrain serve --http on your MagicDNS name with
Tailscale-terminated TLS, provisions the admin bootstrap token in a private
file, installs a launchd / systemd user service so the server survives
reboots, and prints the MCP URL
(https://your-machine.your-tailnet.ts.net/mcp) plus the grant command for
each client. Engine-free — it works while a PGLite brain's serve holds the
write lock. Default reach is your tailnet only; --funnel is the explicit
opt-in for agents that run in a vendor's cloud. Steps, flags, --remove,
and the troubleshooting table: remote MCP guide.
Your AI client
→ https://your-machine.your-tailnet.ts.net/mcp (Tailscale terminates TLS)
→ gbrain serve --http on 127.0.0.1:3131 (user service)
→ Postgres or PGLite
Then provision clients through the running server (the only way on PGLite
while serve is live):
gbrain mcp grant agent-example --harness codex --profile memory-writer --source default \
--url https://your-machine.your-tailnet.ts.net/mcp \
--admin-token-file ~/.gbrain/serve/admin-token --credentials-out /private/agent-example.json
Other tunnels and cloud hosts
Any HTTPS front works: ngrok, Cloudflare Tunnel, or a cloud host (Fly.io,
Railway) for a brain that must answer while your laptop is closed —
ALTERNATIVES.md compares them. You run gbrain serve --http
yourself and pass --public-url whenever the server is reachable at anything
other than http://localhost:<port> so the OAuth issuer in discovery metadata
matches what clients hit (RFC 8414 §3.3).
Remote over OAuth 2.1
gbrain serve --http --port 3131 --public-url https://your-brain.ngrok.app
ngrok http 3131 --url your-brain.ngrok.app
Built-in HTTP transport with OAuth 2.1, scoped operations, an admin dashboard
at /admin, and a live SSE activity feed. Zero external dependencies. This is
the only auth shape that works with ChatGPT (OAuth 2.1 + PKCE is required by
the ChatGPT MCP connector).
Supported clients:
- ChatGPT — requires OAuth 2.1 + PKCE. Works natively with
--http. - Claude Desktop / Cowork — OAuth 2.1 or legacy bearer tokens.
- Perplexity — OAuth 2.1 client credentials grant.
- Claude Code, Cursor, Windsurf — can use OAuth or legacy bearer.
See the OAuth 2.1 setup section below.
Remote with legacy bearer tokens (simplest)
Your AI client (Claude Desktop, Perplexity, etc.)
→ HTTPS front (Tailscale name, ngrok domain, or cloud host)
→ gbrain serve --http (built-in transport with bearer auth)
→ Postgres or PGLite
This requires:
- A machine running
gbrain serve --http(works on both PGLite and Postgres brains) - An HTTPS front (Tailscale via
gbrain mcp expose, ngrok, or a cloud host) - A bearer token created via
gbrain auth create <name>
Bearer tokens created without a scopes grant carry read+write+admin on
the HTTP server; gbrain auth create --scopes read,write mints narrowed
tokens.
OAuth 2.1 Setup
1. Start the HTTP server
gbrain serve --http --port 3131
On first start in an interactive terminal, the server prints an admin bootstrap token to stderr:
Admin bootstrap token: 3a1f9c...
Open http://localhost:3131/admin and paste it to log in.
On a non-TTY start (systemd, Docker, any piped or captured logs) the generated
token is hidden so it never lands in log storage. For headless deploys either
set GBRAIN_ADMIN_BOOTSTRAP_TOKEN to a value you control before starting, or
run gbrain serve --http --print-admin-token once on a trusted terminal to
force printing.
Save this token. Open http://localhost:3131/admin and paste it to access the
dashboard. The dashboard shows live activity, registered clients, request logs,
and per-client config export.
mcp_request_log.paramsand the live SSE activity feed default to a redacted summary{redacted, kind, declared_keys, unknown_key_count, approx_bytes}. Declared param keys are kept (intersected against the operation's spec); unknown keys are counted but never named, and byte sizes round up to 1KB so size-probe attacks can't binary-search secret content. Operators on a personal laptop who want raw payloads back can passgbrain serve --http --log-full-params(loud stderr warning fires at startup). Multi-tenant deployments should leave it on the redacted default.
Owner login links for AI agents
Say to your agent: "Give me the GBrain admin login link" — the agent uses the existing HTTP mint endpoint described below.
When an authenticated owner asks "Give me the GBrain admin login link",
use the existing single-use login flow. A static /admin/ URL opens the login
page; it does not authenticate the owner.
- Confirm the requesting owner and a private destination for the login link.
- Obtain the running server's bootstrap credential through the host's existing
protected credential mechanism.
GBRAIN_ADMIN_BOOTSTRAP_TOKENis the supported deployment setting. Never expose its value in chat, logs, shell arguments, or a URL. If the credential is unavailable, report that specific setup blocker; do not claim the login-link capability is missing. - Send
POST /admin/api/issue-magic-linkto the running server, with the bootstrap credential in theAuthorization: Bearerheader through that protected mechanism. An MCP client bearer token or client secret is not the server bootstrap credential. - The response contains
urlandexpires_in(300 seconds). The returned URL uses the server's configured--public-url; without it, the fallback is localhost. Ensure the deployment has an owner-reachable public URL rather than substituting a remembered tunnel address. - Deliver the returned short-lived login link only to the requesting owner in private. In a shared channel, acknowledge private delivery without reproducing the link. Never put it into a public issue or commit.
Do not GET or fetch the generated login URL to verify it. That redeems the
single-use nonce before the owner can use it. Check the base /admin/ page and
non-secret response metadata separately. The link expires after five minutes,
cannot be replayed, and is invalidated by a server restart. Successful redemption
establishes the admin browser session and redirects to /admin/.
This logs the owner into the dashboard; it does not create, reveal, or rotate an MCP client credential. Register the intended OAuth client separately in the credential-reveal screen below. For unattended deployments, provision the bootstrap credential through the operator's protected configuration before starting the server; generated secrets are deliberately hidden in captured logs.
2. Register OAuth clients
Register clients from the /admin dashboard:
- Click Register client.
- Enter a name (e.g.
perplexity,chatgpt). - Pick scopes:
read,write,admin(checkboxes). - Pick grant type:
client_credentialsfor machine-to-machine (Perplexity, Claude Desktop bearer mode) orauthorization_codefor browser-based clients with PKCE (ChatGPT). - For
authorization_codeclients, paste the redirect URI. - Hit Register. The credential-reveal modal shows the
client_id(andclient_secretfor confidential clients) once. Copy or Download JSON immediately — secrets are hashed on storage and never shown again.
Or from the CLI — faster for scripting:
gbrain auth register-client perplexity \
--grant-types client_credentials \
--scopes "read write"
Source-scoped clients. Multi-source brains can scope a client's write
authority to one source and its read scope to a curated set with the
--source and --federated-read flags:
gbrain auth register-client dept-x-agent \
--grant-types client_credentials \
--scopes "read write" \
--source dept-x \
--federated-read dept-x,shared,parent-canon
--source controls the write authority — put_page / add_link / etc only
land in dept-x. --federated-read controls the read axis independently;
queries return rows from any of the listed sources. Omit both flags for an
unscoped super-client. A client with no recorded source is backfilled to
source_id='default' on gbrain upgrade. Within a source,
slug-level write fencing is also available: --bound-slug-prefixes p1/,p2/
rejects slug-mutating writes outside the listed prefixes (update later with
gbrain auth rescope-client <id> --bound-slug-prefixes <p1,p2|none>).
Host-repo wrappers can register programmatically:
await oauthProvider.registerClientManual(
'perplexity',
['client_credentials'],
'read write',
[], // redirect_uris, empty for CC
);
For self-service client registration (Dynamic Client Registration, RFC 7591),
start the server with --enable-dcr. DCR is off by default.
Say to your agent: "Start my brain's MCP server with self-service client
registration — your agent runs gbrain serve --http --enable-dcr, and you
approve each new connection in the admin UI."
A self-registered client goes through three gates:
- Scope ceiling at registration. Dynamic registration may request at most
read write. A request namingadmin,sources_admin,users_admin, oragentis rejected with HTTP 400invalid_client_metadata(never silently narrowed), and the error text points at the operator path. Under--enable-dcr-insecure, aclient_credentialsregistration is capped atread— a grant that skips owner approval never carrieswrite. While DCR is enabled (either mode), OAuth discovery advertises theread writeself-registration ceiling asscopes_supported(authorization-server and protected-resource metadata alike), so a client that registers with the advertised scopes succeeds; with DCR off, discovery lists every scope an operator-registered client may hold.agentis never advertised — it needs delegation bindings no OAuth request can carry. - Owner approval on
/authorize. Every authorization-code connection redirects to the admin dashboard, where you see the client, its redirect URI, and the requested scopes, and approve or deny. No code is minted until you approve. Consent never widens the registered scope. - Per-request clamp. Issued codes and tokens are re-intersected with the
client's current registered scope, so a later
rescope-clienttakes effect on the next request.
To give a self-registered client more than read write, widen it yourself
after the fact — gbrain auth rescope-client <client_id> --scopes read,write,sources_admin
(or the admin dashboard's Agents page) — or pre-register it with
gbrain auth register-client / the admin API, which accept every scope.
gbrain doctor warns about active clients that hold a privileged scope but
look self-registered.
Native MCP clients register cleanly: redirect_uris may use an app custom
scheme (RFC 8252, e.g. myapp://callback) or http:// loopback alongside
https://; scopes the server doesn't know are filtered rather than fatal;
and malformed registration metadata is rejected with HTTP 400
invalid_client_metadata (never a 500), so a client can correct and retry.
DCR requests may include an optional token_ttl_seconds field (integer,
seconds) to request a per-client access-token lifetime. The server clamps the
request into an admin-configured window — never rejects over it — persists the
effective value as the client's TTL override, and echoes it back as
token_ttl_seconds in the registration response. Subsequent /token responses
for that client carry the matching expires_in. Clients that omit the field
keep the server default (--token-ttl). The window defaults fail-closed: min
300 seconds, max bounded by your --token-ttl — a self-registering client
cannot request a longer-lived token than the server default unless you
explicitly widen the window:
gbrain config set oauth.dcr_ttl_min_seconds 600
gbrain config set oauth.dcr_ttl_max_seconds 86400
3. Expose the server
Tailscale (recommended). gbrain mcp expose does this whole section for
you — Tailscale install/login, tailscale serve (or funnel), the admin
token file, a user service, and the health checks — and keeps the server on
its default loopback bind, because Tailscale terminates TLS on the tailnet
and forwards to 127.0.0.1. The manual equivalent is the
Tailnet / LAN-only shape below. Full
walkthrough: remote MCP guide.
ngrok (alternative). ngrok also connects to loopback on the same machine, so the default bind is right there too:
gbrain serve --http --port 3131 --public-url https://your-brain.ngrok.app
Bind explicitly only when the front is on another host. gbrain serve --http defaults to 127.0.0.1. To accept connections from a reverse proxy
or tunnel agent running on a different machine (or a container), restart with
--bind:
gbrain serve --http --port 3131 --bind 0.0.0.0 --public-url https://brain.example.com
When --public-url is set without --bind, a stderr WARN fires at
startup so a real misconfiguration ("the tunnel is up but my agent gets
ECONNREFUSED") is loud; in the Tailscale and same-machine-ngrok shapes the
WARN is expected and harmless. Binding 0.0.0.0 without
GBRAIN_HTTP_CORS_ORIGIN warns too: browser-based clients get no CORS header
until you set the allowlist (see SECURITY.md — CORS).
--source-guard is a stdio-lane flag: with --http it prints a warning and
is ignored. HTTP writes are fenced by each token's scopes instead, so
operators migrating from stdio mint narrowed tokens
(gbrain auth create <name> --scopes read) rather than relying on the guard.
For the ngrok alternative, start the tunnel after the server:
brew install ngrok
ngrok config add-authtoken YOUR_TOKEN
ngrok http 3131 --url your-brain.ngrok.app
Your OAuth issuer URL is whatever --public-url names
(https://your-machine.your-tailnet.ts.net under Tailscale,
https://your-brain.ngrok.app under ngrok). The MCP SDK's
router exposes the spec-compliant discovery endpoint at
/.well-known/oauth-authorization-server. The protected resource is the
/mcp endpoint itself: its RFC 9728 metadata is served at
/.well-known/oauth-protected-resource/mcp (the bare
/.well-known/oauth-protected-resource root stays as an alias for older
clients), and every 401 carries WWW-Authenticate: Bearer resource_metadata="<that URL>", so an MCP client pointed at
https://your-machine.your-tailnet.ts.net/mcp finds the token endpoint from a
fresh connection without any pasted URLs.
Dual-mode auth on /mcp. The same route verifies OAuth 2.1 access
tokens and gbrain auth create bearers (OAuth first, then the
access_tokens fallback). The 401 + resource_metadata challenge is emitted
by the MCP SDK middleware for ANY request lacking an Authorization header
(RFC 9728 / MCP auth spec §5.1 discovery) and says nothing about whether a
configured token works. A client status probe that omits the header will
therefore report needsAuth / authentication_required even while the
configured bearer succeeds. Judge auth from whoami
(transport: legacy|oauth) or gbrain auth test <url> --token <t>; treat a
client's needsAuth flag as advisory unless the authenticated call itself
returns 401 / invalid_token.
Tailnet / LAN-only (no public tunnel)
Two shapes work without exposing anything to the internet. In both, clients
authenticate with gbrain auth create bearer tokens.
Tailscale Serve (HTTPS, tailnet-only). This is what gbrain mcp expose
automates (plus the admin token file, the user service and the health
checks). By hand: keep the default 127.0.0.1 bind, let Tailscale terminate
TLS on the tailnet, and point --public-url at your MagicDNS name:
gbrain serve --http --port 3131 --public-url https://your-machine.your-tailnet.ts.net
tailscale serve --bg 3131
Clients on the tailnet use https://your-machine.your-tailnet.ts.net/mcp.
The "--public-url is set but --bind is not" WARN is expected in this shape —
Tailscale Serve forwards to loopback. tailscale serve stays inside your
tailnet; tailscale funnel (or gbrain mcp expose --funnel) is public
exposure for cloud agents (see ALTERNATIVES.md).
Plain HTTP, bearer-only. Bind the tailnet/LAN interface and omit
--public-url entirely:
gbrain serve --http --port 3131 --bind 100.x.y.z # or --bind 0.0.0.0
The OAuth issuer defaults to http://localhost:3131, which the MCP SDK
accepts, and bearer-token verification never reads the issuer. Clients connect
to http://100.x.y.z:3131/mcp with Authorization: Bearer …. OAuth discovery
is the one thing this shape does not offer (the advertised issuer is
loopback), so OAuth-only clients such as ChatGPT need the HTTPS shape above.
Passing --public-url http://100.x.y.z:3131 instead fails at startup — see
Troubleshooting.
4. Scopes and localOnly
Every operation is tagged read | write | admin. Operations flagged
localOnly: true in src/core/operations.ts (sync_brain and the
file_* ops among them) are rejected over HTTP regardless of scope.
Remote agents cannot reach local filesystem surface area.
| Scope | What it allows |
|---|---|
read | search, query, get_page, list_pages, graph traversal |
write | put_page, delete_page, add_link, add_timeline_entry |
admin | Client management, token revocation, sweep; local-only restrictions still apply |
Write ops can additionally be fenced per client with --bound-slug-prefixes
(see Register OAuth clients above).
Legacy Bearer Token Setup
Bearer tokens are the simple path when you don't need per-client scoping.
Without a --scopes grant they carry read+write+admin on the
HTTP server; pass --scopes read,write at creation to narrow one.
1. Publish the server
Tailscale (recommended): gbrain mcp expose (add --funnel for cloud
clients) — remote MCP guide. ngrok alternative,
see the ngrok-tunnel recipe; quick version:
gbrain serve --http --port 8787
brew install ngrok
ngrok config add-authtoken YOUR_TOKEN
ngrok http 8787 --url your-brain.ngrok.app # Hobby tier for fixed domain
2. Create access tokens
# Create a token for each client
gbrain auth create "claude-desktop"
# List all tokens
gbrain auth list
# Revoke a token
gbrain auth revoke "claude-desktop"
Tokens are per-client. Create one for each device/app. Names are not
unique: gbrain auth revoke "<name>" revokes EVERY active token carrying
that name — use gbrain auth list (shows each token's id and scopes) and
gbrain auth revoke --id <uuid> to revoke exactly one. Tokens are stored
SHA-256 hashed in your database.
3. Connect your AI client
- ChatGPT: setup guide (OAuth 2.1 + PKCE, requires
gbrain serve --http) - Claude Code: setup guide
- Claude Desktop: setup guide (must use GUI, not JSON config)
- Claude Cowork: setup guide
- Perplexity: setup guide
4. Verify
gbrain auth test \
https://your-machine.your-tailnet.ts.net/mcp \
--token YOUR_TOKEN
(Substitute your ngrok domain or cloud host URL when you used one of those.)
Operations
GBrain's operation catalog (100+ operations in src/core/operations.ts) is
available subject to the selected surface, scope and operation-specific limits.
Operations flagged localOnly: true are rejected over HTTP regardless of scope
(see Scopes and localOnly above). Code-inspection
operations and stored contradiction reports also have temporary local-only
restrictions, even when listed in the catalog. The MCP surface runbook
explains these limits and the separate chunk-rebuild requirement. Rebuild
indexes from a local installation on the brain host; a thin client cannot
rebuild the host's indexes.
Several brains behind one tool catalog? Give each server an identity so a
connected agent can tell them apart: gbrain config set mcp.instructions "<identity>" rides the initialize response of every transport (stdio, OAuth
HTTP, legacy bearer) under a Deployment identity: banner, appended below the
canonical agent contract — no transport can weaken the contract. Restart
gbrain serve after setting it (the response is built once per process);
GBRAIN_MCP_INSTRUCTIONS in the serve process's environment overrides the
configured value for that process, and a blank variable falls back to it.
Say to your agent: "Tell connected agents which brain this is" — your
agent runs gbrain config set mcp.instructions "<identity>".
Security note on file access: the file_* operations being localOnly is
the first line of defense; as defense-in-depth, file_upload also confines
any caller that isn't verifiably the trusted local CLI to the working
directory where gbrain serve was launched. Symlinks, .. traversal, and
absolute paths outside cwd are rejected, and page slugs and filenames are
allowlist-validated (alphanumeric + hyphens; no control chars, RTL overrides,
or backslashes). Local CLI callers (gbrain files upload ...) keep
unrestricted filesystem access since the user owns the machine.
Deployment Options
Tailscale via gbrain mcp expose is the recommended shape for a brain on your
own computer (remote MCP guide). See
ALTERNATIVES.md for how Tailscale Serve / Funnel compare
with ngrok and always-on cloud hosts (Fly.io, Railway).
Co-located Docker workloads (self-hosted Postgres)
OAuth scopes and source scoping guard the gbrain serve --http path. They do
NOT guard raw Postgres. If the brain's Postgres runs as a container on the same
Docker host as other workloads (agent runtimes, n8n, staging fixtures), any
container sharing Docker's default bridge network can open a direct DB
session — no OAuth token required — and read every source. That silently
recreates a privileged path underneath the isolation you configured at the MCP
layer.
Network-zone the host so untrusted containers can never reach Postgres:
Docker host
├── gbrain-net ← ONLY the brain's Postgres (+ gbrain serve, if containerized)
├── agent-<id>-net ← each untrusted agent runtime, isolated
└── default bridge ← no secret-bearing databases
Operator checklist:
[ ] Postgres is on a user-defined Docker network, not the default bridge
(or nothing else runs on that bridge)
[ ] If Postgres publishes a host port at all, it binds loopback only
(`-p 127.0.0.1:5432:5432`, never `0.0.0.0`)
[ ] Untrusted agent containers have no DATABASE_URL or Postgres password
[ ] Untrusted agents reach the brain via OAuth/Bearer against serve --http only
(host loopback via host.docker.internal / host gateway — never gbrain-net)
[ ] OAuth clients are least-privilege: scoped --source / --federated-read,
pre-minted short-lived tokens preferred over long-lived client secrets
[ ] Isolation verified: a team-scoped client cannot read internal-only sources
Optional defense-in-depth: a dedicated Postgres role (or RLS) limited to the
allowed source_ids, so even a leaked connection string can't read everything.
Run gbrain under a real init (tini / --init)
If gbrain serve is your container's entrypoint, it runs as PID 1 and
inherits every orphaned process in the container. Prefer a real init so
orphan exits are reaped by something built for the job:
# Dockerfile: wrap the entrypoint with tini
ENTRYPOINT ["/usr/bin/tini", "--", "gbrain", "serve", "--http"]
or at run time:
docker run --init ... gbrain serve --http
Without an init, gbrain installs its own PID-1 orphan reaper (Linux only):
a low-frequency /proc scan that waitpid()s zombies re-parented to it,
so long-lived containers don't accumulate defunct entries in the PID table.
It is fail-open and can be disabled with GBRAIN_PID1_REAP=0 — but tini /
--init remains the recommended setup.
Troubleshooting
"missing_auth" error
Include the Authorization header: Authorization: Bearer YOUR_TOKEN
"invalid_token" error
Run gbrain auth list to see active tokens.
Client status shows needsAuth / authentication_required but tool calls succeed
The client probed /mcp without an Authorization header and read the
spec-mandated discovery 401 as a failed login. Both OAuth tokens and legacy
bearers are accepted on /mcp; confirm with whoami (transport: legacy)
or gbrain auth test <url> --token <t> and only re-authenticate if THAT
call returns 401. See
Dual-mode auth on /mcp.
"service_unavailable" error Database connection failed. Check your Supabase dashboard for outages.
"Issuer URL must be HTTPS" at startup
The MCP SDK rejects a non-HTTPS OAuth issuer unless the host is localhost
or 127.0.0.1, so --public-url http://<lan-or-tailnet-ip>:3131 exits
before the server listens. Either terminate TLS in front (Tailscale Serve,
ngrok, Cloudflare Tunnel) and pass the https:// URL, or drop --public-url
for a bearer-only LAN endpoint — both shapes are in
Tailnet / LAN-only. Last resort, for
plain-HTTP OAuth discovery on a private network you fully control: the SDK's
own MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL=1 opt-in. Bearer auth works
either way; OAuth clients may still refuse a non-HTTPS issuer.
Claude Desktop doesn't connect
Remote servers must be added via Settings > Integrations, NOT
claude_desktop_config.json. See CLAUDE_DESKTOP.md.
gbrain mcp expose stops on a Tailscale error or verify.tailnet: pending
The pre-checks (tailscale_https_not_enabled, tailscale_funnel_not_enabled
— exit 2 with the admin-console URL, nothing published), the classified
errors (https_not_enabled, funnel_not_enabled, needs_operator,
daemon_not_running, …), the pending-certificate wait, and the
service/--status checks are tabled in the
remote MCP guide.
Expected Latencies
| Operation | Typical Latency | Notes |
|---|---|---|
| get_page | < 100ms | Single DB query |
| list_pages | < 200ms | DB query with filters |
| search (keyword) | 100-300ms | Full-text search |
| query (hybrid) | 1-3s | Embedding + vector + keyword + RRF |
| put_page | 100-500ms | Write + trigger search_vector update |
| get_stats | < 100ms | Aggregate query |
Note: gbrain serve --http has OAuth 2.1 + the admin dashboard baked
into the binary. The custom HTTP wrapper pattern (see
voice recipe) is supported for
teams that need bespoke middleware, but for most remote deployments the
built-in server is the recommended path.