Add MCP tools for headless Google Account pairing - #162
Open
domdomegg wants to merge 1 commit into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Google Account (Gaia) pairing is the working pairing path —
CLAUDE.mdnotes QR is dead — but it's only reachable throughopenmessage 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 opaquepairing_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.PairingSessionholds 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.go—StartGaiaPairing/FinishGaiaPairing, thin wrappers over the two libgm methodsDoGaiaPairingalready composes, plus the cookie parsing lifted fromcmd/pair.goso the CLI and the tools share one implementation.PairWithGoogleCookiesremains 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 tointernal/client; keeps a thinparseGoogleCookiesInputalias so its existing tests are unchanged.curl -b '...'— the form Chrome's "Copy as cURL" actually produces formessages.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_pairingreturned the emoji, tapping it on the phone letcomplete_google_pairingwritesession.json, and messages were then readable through the other tools.Notes
cmd/pair_test.go.github.com/google/uuidmoves from an indirect to a direct dependency (it was already in the module graph).🤖 Generated with Claude Code