ADR 0017 — In-app Google (Gmail + Calendar) connect flow
- Status: Accepted (2026-06-03)
- Date: 2026-06-03
- Deciders: Josh Mabry; protoAgent maintainers
- Tags: surface, google, oauth, mcp, onboarding, operator, desktop, opt-in
- Related: follows ADR 0016 (in-app Discord config) as the second UI-driven integration; the surface is the Google MCP server (Slice 2); uses the secrets-overlay + settings-schema patterns from ADR 0013.
Accepted. The Google surface (Gmail + Calendar) existed but its setup was file + CLI — download
credentials.json, runpython -m mcp_servers.google.serverfor consent, hand-editmcp.servers. Unreachable from the desktop app (no shell, nopythonon PATH, read-only frozen bundle), so the agent simply had no calendar/mail. Google is now connected in-app: paste a Desktop-app OAuth client, click Connect Google, approve in the browser — token cached per-user, MCP auto-wired.
1. Context & Problem statement
mcp_servers/google/ is a good MCP server, but onboarding assumed a developer at a shell: a credentials.json file, a blocking run_local_server consent run from the CLI, and a manual mcp.servers entry. In the bundled desktop app none of that is possible — and the result is the agent telling the operator "I can't read your calendar." The same gap ADR 0016 closed for Discord, now for Google — with the extra wrinkle of an OAuth consent dance.
2. Decision
- Config + secrets. A
googlesection (enabled,client_id,client_secret→ secrets.yaml,tz). The OAuth client comes from the operator's Google Cloud Desktop app client — entered in the UI, not a file. - Connect flow.
POST /api/config/google/connectruns the installed-app consent (InstalledAppFlow.run_local_server, which opens the operator's browser + loopback-captures the grant) off the event loop, caches a refreshable token in the per-user config dir, enables the surface, and reloads so the tools register.GET /api/config/google/statusreports{configured, connected, email}for the UI. A "Connect Google" button in Settings (+ an OAuth-client step in the wizard) drives it. - Managed MCP server. When google is enabled and a token is cached, the config layer auto-injects the google MCP server entry (the operator never edits
mcp.servers) and forces MCP on. The headless subprocess is load-only — it never runs consent (no surprise browser); consent is the explicit Connect action. - Frozen desktop launch. The bundled binary has no
python, so the managed entry re-invokes the binary itself (<binary> --mcp-google) instead ofpython -m mcp_servers.google.server. The Google libs + MCP SDK are bundled into the sidecar. - Scope. Read Gmail + Calendar, draft-only mail (no send) — unchanged from Slice 2. Env/
credentials.jsonremain a Docker/headless fallback.
3. Consequences
- The desktop app can connect Google with no files, no CLI, no
mcp.serversediting — the operator pastes a client and clicks Connect. - The token lives per-user (config dir), so it works in the read-only frozen bundle and isn't shared across forks.
- The sidecar grows by the Google client libraries (bundled). Acceptable for an out-of-the-box integration; still opt-in (off until connected).
- The OAuth client is still the operator's own (their Google Cloud project) — the template ships the flow, not shared credentials (quota/security).
4. Alternatives considered
- Keep it file/CLI, document harder. Rejected — unreachable for the desktop app's target user.
- Ship a shared OAuth client in the template. Rejected — shared quota + verification + secret-distribution problems; each operator using their own Desktop-app client is the Google-sanctioned path.
- In-process Google tools instead of the MCP subprocess. Tempting (no frozen-subprocess problem), but it abandons the MCP-server design Slice 2 deliberately chose as the worked MCP example. The
--mcp-googleself-reinvoke keeps the architecture and works frozen.