Skip to content

Add MCP tools for headless Google Account pairing - #162

Open
domdomegg wants to merge 1 commit into
MaxGhenis:mainfrom
domdomegg:pair-with-google-cookies-tool
Open

Add MCP tools for headless Google Account pairing#162
domdomegg wants to merge 1 commit into
MaxGhenis:mainfrom
domdomegg:pair-with-google-cookies-tool

Conversation

@domdomegg

Copy link
Copy Markdown

Problem

Google Account (Gaia) pairing is the working pairing path — CLAUDE.md notes QR is dead — but it's only reachable through openmessage pair --google, which reads cookies from stdin and prints the confirmation emoji to a terminal.

That leaves a gap for headless installs, which the Dockerfile explicitly targets ("Useful when you want to keep the inbox running on a home server / NAS"): a deployment driven entirely over MCP can't pair or re-pair without shell access to the host. Since Google sessions need periodic re-pairing, that's a recurring need, not just first-run setup.

Why two tools rather than one

Gaia pairing is a two-party handshake: Google returns a confirmation emoji, then blocks until the user taps it on their phone.

That doesn't fit in a single tool call. A call is one request/response, so the emoji is only readable once the call returns — by which point the user already needed it. I tried the one-tool version first and it fails exactly this way: it times out, with the user never learning which emoji to look for.

So it's split, mirroring the start/complete shape common to OTP flows:

  • start_google_pairing(cookies) — returns immediately with the emoji and an opaque pairing_handle, so the emoji can be surfaced while the tap is still pending.
  • complete_google_pairing(pairing_handle) — waits for confirmation (2 min) and saves the session.

libgm.PairingSession holds an ECDSA private key bound to its client, so unlike an opaque OTP challenge token it can't round-trip through the caller. It's kept in a server-side registry keyed by a UUID handle — single-use, with a 10 minute TTL that disconnects abandoned clients.

Changes

  • internal/client/gaiapair.goStartGaiaPairing / FinishGaiaPairing, thin wrappers over the two libgm methods DoGaiaPairing already composes, plus the cookie parsing lifted from cmd/pair.go so the CLI and the tools share one implementation. PairWithGoogleCookies remains for the CLI, where the emoji can be printed mid-handshake.
  • internal/tools/pair_google.go — the two tools and the pairing registry.
  • cmd/pair.go — now delegates to internal/client; keeps a thin parseGoogleCookiesInput alias so its existing tests are unchanged.
  • Cookie parsing gains a test for curl -b '...' — the form Chrome's "Copy as cURL" actually produces for messages.google.com. Only -H 'Cookie:' was covered before, and this was a real failure when testing against a live account.

Testing

go test ./... passes. The new tools have unit coverage for the emoji/handle round trip, blank and malformed cookies, unknown and reused handles, TTL eviction, the timeout path, and all five accepted cookie input shapes.

Verified end-to-end against a real Google account over MCP stdio: start_google_pairing returned the emoji, tapping it on the phone let complete_google_pairing write session.json, and messages were then readable through the other tools.

Notes

  • No behaviour change to the CLI — the refactor is a pure move, still covered by the existing cmd/pair_test.go.
  • github.com/google/uuid moves from an indirect to a direct dependency (it was already in the module graph).
  • Happy to adjust naming, the TTL/timeout values, or split the refactor into its own commit if you'd prefer.

🤖 Generated with Claude Code

Google Account (Gaia) pairing is the working pairing path — CLAUDE.md notes
QR is dead — but it was reachable only through `openmessage pair --google`,
which reads cookies from stdin and prints the confirmation emoji to a terminal.
A headless install driven over MCP had no way to pair or re-pair without shell
access to the host.

Gaia pairing is a two-party handshake: Google returns a confirmation emoji,
then blocks until the user taps it on their phone. That does not fit in one
tool call — a call is a single request/response, so the emoji is only readable
once the call returns, by which point the user needed it. In practice such a
tool just times out with the user never learning which emoji to look for.

So it is two tools, mirroring the start/complete shape common to OTP flows:

- start_google_pairing(cookies) returns immediately with the emoji and an
  opaque pairing_handle, so the emoji can be surfaced while the tap is pending.
- complete_google_pairing(pairing_handle) waits for confirmation (2 min) and
  saves the session.

libgm's PairingSession holds an ECDSA private key bound to its client, so
unlike an opaque OTP challenge token it cannot round-trip through the caller.
It is kept in a server-side registry keyed by a UUID handle, single-use, with a
10 minute TTL that disconnects abandoned clients.

internal/client gains StartGaiaPairing / FinishGaiaPairing — thin wrappers over
the two libgm methods DoGaiaPairing already composes — plus the cookie parsing
lifted from cmd/pair.go so the CLI and the tools share one implementation.
PairWithGoogleCookies remains for the CLI, where the emoji can be printed
mid-handshake. cmd/pair.go keeps a thin parseGoogleCookiesInput alias so its
existing tests are unchanged.

Cookie parsing gains a test for `curl -b '...'`, the form Chrome's "Copy as
cURL" actually produces for messages.google.com; only `-H 'Cookie:'` was
covered before.

Verified end-to-end against a real Google account over MCP stdio: emoji
returned by the first call, tapped on the phone, session.json written by the
second, and messages subsequently readable through the other tools.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant