fix: Google Workspace MCP connectors - durable refresh tokens + reliable OAuth popup
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
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 OIDCoffline_accessscope 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 (theencrypted_refresh_tokencolumn stayed null, so there was nothing to renew with).OAuth popup completion was mis-detected under COOP. Google's consent page is served with
Cross-Origin-Opener-Policy: same-origin, which severswindow.openerand makespopup.closedunreadable from the opener. As a result the callback'swindow.opener.postMessagenever reached the app, and a COOP-blockedpopup.closedread 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 inredirectToAuthorization()(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.comor.googleapis.com), so look-alikes likenotgoogleapis.comare 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), annpm testscript, and exclude test files from the build.
Frontend - connectors/page.tsx (commit bfb5c11)
- Detect OAuth completion by polling the backend's
oauthConnectedflag (the source of truth) instead of relying on COOP-brokenpostMessage/popup.closed. KeeppostMessageas a fast path for providers that don't sever the opener, and drop the false-positivepopup.closedrejection.
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_tokennow populated) - confirmed via both the API and the UI. - Live
tools/listagainstdrivemcp.googleapis.comreturns the Drive tools. - The UI connect flow now completes cleanly with the popup fix (no false "window closed").
- The live backend builds the Google authorization URL with
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
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.