Documentation

OAuth

Remote MCP auth with PKCE and DCR.

Set [oauth].callback_base in config.toml so acp-host can complete browser OAuth.
Public remotes work without it; authenticated gateways need the callback (or /mcp code paste).

Tokens are never written to the repo. They live under:

$state_dir/mcp-oauth/by-repo/<repoKey>/<id>.json   # mode 0600
$state_dir/mcp-oauth/pending/                      # PKCE in flight

Default state_dir is ~/.local/share/acpbot/state (see Configuration).

Shared state dir

/mcp auth runs in the Telegram worker and writes pending PKCE.
GET /oauth/callback and session ensure run on acp-host.

Both processes must use the same config file (or the same absolute state_dir). Boot logs print the resolved path on both processes.

Setup

Listener always uses port 8788. Selecting MagicDNS only switches the scheme to https and loads Tailscale certs — it does not bind :443.

[oauth]
callback_base = "https://your-node.ts.net:8788"   # phone on the same tailnet
# listen_port = 8788   # default (same for http and https)
# tls_cert / tls_key optional — auto-detected from ~/.local/share/tailscale-certs/

Issue a cert once (macOS and Linux — same paths):

mkdir -p ~/.local/share/tailscale-certs
cd ~/.local/share/tailscale-certs
tailscale cert your-node.ts.net
FilePath
Certificate~/.local/share/tailscale-certs/<MagicDNS>.crt
Private key~/.local/share/tailscale-certs/<MagicDNS>.key

acpbot auto-detects those files from callback_base (or tailscale status).
You can also set them explicitly:

[oauth]
callback_base = "https://your-node.ts.net:8788"
tls_cert = "~/.local/share/tailscale-certs/your-node.ts.net.crt"
tls_key  = "~/.local/share/tailscale-certs/your-node.ts.net.key"

Plain HTTP fallback

[oauth]
callback_base = "http://100.x.y.z:8788"   # Tailscale IP or LAN — no cert needed
# listen_host = "0.0.0.0"
# listen_port = 8788

Guided setup detection

acpbot setup offers a picker for callback_base. It detects hosts when possible (via tailscale status --json and local network interfaces) and always allows a custom URL:

OptionSourceExample
Tailscale HTTPSSelf.DNSName (MagicDNS) + local certshttps://your-node.ts.net:8788
Tailscale IPTailscale 100.x IPv4http://100.64.1.2:8788
LAN IPPrivate interface addrs (10.x, 172.16–31.x, 192.168.x)http://192.168.1.10:8788
Custom URL…Manual entrytunnel / Serve / Funnel
Skip / clearUnsetuse /mcp code paste fallback
  • All options use port 8788. MagicDNS is https://…:8788 (TLS); IP options stay http://…:8788.
  • If certs are missing, setup prints the tailscale cert commands (table of .crt / .key paths).
  • Prefer Tailscale HTTPS when the phone is on the same tailnet. LAN IPs only work on the same Wi‑Fi/Ethernet.
  • Detection + cert helpers: src/setup/oauth-callback-detect.ts (tests in test/oauth-callback-detect.test.ts).

Run:

acpbot host      # serves GET /oauth/callback when callback_base is set
acpbot worker    # same config.toml / state_dir
# from source: acpbot host · acpbot worker

If bind fails (port in use), acp-host exits with a clear error when callback_base is set. Free the port or use the paste fallback below.

Operator flow

  1. In a session topic: /mcp add <id> <url>
    → acpbot attaches a per-topic mcp-proxy immediately (empty tool list until auth).
  2. /mcp auth <id>
  3. Open the tappable authorize URL in Telegram (host does not open a browser)
  4. On callback, PKCE completes; token is stored under state_dir (not the repo)
  5. The live proxy re-reads the store, connects upstream, and advertises tools — no agent restart
  6. Pending PKCE expires after 15 minutes
  7. Access tokens auto-refresh when stale (stored refresh_token + token endpoint). On 401 the proxy force-refreshes. If refresh fails (invalid_grant, no refresh token), run /mcp auth <id> again — still no agent restart.

Remotes are always served via acpbot mcp-proxy (stdio, per session slot). Details: MCP.

Before auth the agent still sees the MCP server name, but with zero tools. After auth (or reauth), tools appear without respawning the agent.

Paste fallback

If the redirect cannot reach the host:

  1. Prefer /mcp code <full-callback-url> (includes code + state)
  2. Last resort: /mcp code <code> <id>

Discovery (no env client_id / auth URL)

On /mcp auth, acpbot:

  1. Probes the MCP URL for WWW-Authenticate resource_metadata (RFC 9728), else fetches /.well-known/oauth-protected-resource…
  2. Loads authorization-server metadata (RFC 8414)
  3. Dynamically registers a public PKCE client (registration_endpoint, RFC 7591)
  4. Opens authorize with the registered client_id + resource indicator

The gateway must publish AS metadata with a registration endpoint. There are no per-gateway CLIENT_ID / AUTH_URL config keys.

Security model

PieceNote
Listen addressDefault 0.0.0.0 so phone redirects work
Who can hit the portAnyone who can reach the host may attempt a callback
Real protectionHigh-entropy state + PKCE (code_verifier never leaves the host)
Network preferenceTailscale Serve / tailnet over public Funnel/IP

Implementation map

AreaPath
Discoverysrc/mcp/oauth-discovery.ts
PKCE / flowsrc/mcp/oauth-pkce.ts, oauth-flow.ts
Token storesrc/mcp/oauth-store.ts
Per-slot stdio proxy (official MCP TS SDK)src/mcp/proxy.ts, proxy-rewrite.ts
HTTP callbacksrc/acp-host/oauth-http.ts
Teststest/mcp-oauth.test.ts, test/mcp-proxy-rewrite.test.ts

Env overrides (ACPBOT_OAUTH_CALLBACK_BASE, ACPBOT_OAUTH_*) work when set; prefer TOML for day-to-day use.