fix: Google Workspace MCP connectors - durable refresh tokens + reliable OAuth popup

🟢 open · #185 · Open-Legal-Products/mike ← hordruma/mike · opened 1mo ago by hordruma · +241-24 across 5 files · ↗ on GitHub

From the PR description

Summary

Google Workspace MCP connectors (Drive / Gmail / Calendar on *.googleapis.com) were unusable past the first hour: they authorized once and then broke when the short-lived access token expired, and the OAuth connect popup could report a false failure. This PR makes the connection durable and the connect flow reliable.

Problems

  1. No refresh token was ever obtained. Google only issues a refresh token when the authorization request opts into offline access (access_type=offline) and is forced to re-prompt for consent (prompt=consent), and it does not implement the OIDC offline_access scope the MCP SDK would otherwise use. The SDK builds a spec-compliant authorization URL and exposes no hook for these proprietary params, so a Google connector got only an access token and broke as soon as it expired (the encrypted_refresh_token column stayed null, so there was nothing to renew with).

  2. OAuth popup completion was mis-detected under COOP. Google's consent page is served with Cross-Origin-Opener-Policy: same-origin, which severs window.opener and makes popup.closed unreadable from the opener. As a result the callback's window.opener.postMessage never reached the app, and a COOP-blocked popup.closed read reports a false "closed" - surfacing a spurious "OAuth authorization window was closed" error even though the backend had already exchanged the code and stored the tokens.

Changes

Backend - backend/src/lib/mcp/oauth.ts (commit f9b5bf1)

  • Add providerAuthorizationParams() policy returning { access_type: "offline", prompt: "consent" } for Google hosts, applied generically in redirectToAuthorization() (the SDK's only authorization-URL seam) - the redirect path carries no Google specifics, so future providers are a one-line policy change.
  • Add/share isGoogleOAuthHost() with tightened matching (=== googleapis.com or .googleapis.com), so look-alikes like notgoogleapis.com are no longer treated as Google.
  • Leave scope resolution to the SDK (documented): it falls back to the scopes the MCP server advertises; passing auth-server scopes would request the wrong (generic OIDC) scopes for Google.
  • Align the frontend isGoogleMcpConnector() host check.
  • Add backend tests (oauth.test.ts), an npm test script, and exclude test files from the build.

Frontend - connectors/page.tsx (commit bfb5c11)

  • Detect OAuth completion by polling the backend's oauthConnected flag (the source of truth) instead of relying on COOP-broken postMessage / popup.closed. Keep postMessage as a fast path for providers that don't sever the opener, and drop the false-positive popup.closed rejection.

Testing

  • Backend unit tests: 7/7 pass.
  • Verified end-to-end against a real Google Workspace account:
    • The live backend builds the Google authorization URL with access_type=offline + prompt=consent, the correct Drive/Gmail scopes, PKCE (S256), and the right redirect URI.
    • Completed real consent → refresh token obtained and stored (previously-null encrypted_refresh_token now populated) - confirmed via both the API and the UI.
    • Live tools/list against drivemcp.googleapis.com returns the Drive tools.
    • The UI connect flow now completes cleanly with the popup fix (no false "window closed").

Notes

  • Write / destructive tools (e.g. Gmail create_draft) remain intentionally gated behind the existing "confirmation required" flag; this PR does not change that behavior.

🤖 Generated with claude-flow

https://claude.ai/code/session_01HSWSVK1PQozcrmZGgBhFXS

Our analysis

Make Google Workspace MCP connectors survive token expiry — read the full analysis →

Think the analysis missed something the PR description covers?

Capture this PR into my fork

Download a Markdown prompt that tells Claude how to port every commit in this PR into your working tree. Run it via claude -p < capture-pull-185.md from inside the repo you want the changes in.

⬇ Download capture-pull-185.md