@a1-x-tech/mcp-google-auth
v0.1.0
Published
Reusable Google OAuth onboarding for the mcp-google-* MCP server line: in-chat login tools (loopback + PKCE) and a per-request TokenProvider.
Maintainers
Readme
@a1-x-tech/mcp-google-auth
Reusable Google OAuth onboarding for the mcp-google-* MCP server line. One
package gives every server the in-chat login of the Yandex line — adapted to
Google's rules: a loopback listener on 127.0.0.1 + PKCE (S256) instead of
the dead OOB flow, a user-owned Desktop OAuth client (personal-use
exemption — no verification, no CASA), and a set_client path so the
client_secret never passes through the chat.
This is a library, not a server: it registers six MCP tools on your
McpServer and hands back a TokenProvider your API client calls per request.
Usage
import { registerGoogleAuth, unconfiguredPrefix } from "@a1-x-tech/mcp-google-auth";
const options = {
serverName: "calendar", // → ~/.config/mcp-google-calendar/credentials.json
envPrefix: "GOOGLE_CALENDAR", // → GOOGLE_CALENDAR_REFRESH_TOKEN, …
scopes: ["https://www.googleapis.com/auth/calendar.events"],
// verifyIdentity?: async (accessToken) => ({ email }), // default: OIDC userinfo
// defaultClient?: { clientId, clientSecret? }, // built-in A1 client slot
};
const provider = registerGoogleAuth(server, options);
// In your API client, per request:
const token = await provider.getAccessToken();
// On a 401 from the API — one re-mint + replay:
const fresh = await provider.getAccessToken(true);
// Before a tool that needs a specific scope (partial grants are stored, not rejected):
provider.assertScopes(["https://www.googleapis.com/auth/calendar.events"]);
// At startup, when provider.hasToken() is false, prepend to the initialize instructions:
const instructions = unconfiguredPrefix(options) + REGULAR_BRIEFING;The component always adds openid + userinfo.email (non-sensitive) to
scopes — that is how finish_login names the account even for APIs without
an identity call.
Registered tools
| Tool | Hint | Purpose |
|---|---|---|
| auth_status | read-only | Connection state, token source, expiry, granted/missing scopes, paths. No network, no token in the output. |
| setup_instructions | read-only | GCP wizard checklist (project → API → consent screen → Publish app → Desktop client → Download JSON). Shortens when a client already exists. |
| set_client | write | Reads the downloaded client_secret_*.json by path, validates type installed, stores it in the shared client.json (0600). The secret never enters the chat. |
| start_login | write, deliberately not read-only | Mints PKCE + state, binds a one-shot listener on 127.0.0.1 (random port or <PREFIX>_OAUTH_PORT), returns authorizeUrl + nextStep. Does not open the browser. A read-only hint would let a prompt injection start an OAuth flow silently. |
| finish_login | write | Awaits the local code exchange, saves tokens atomically, verifies identity (server callback or userinfo), reports accountEmail, grantedScopes, missingScopes, previousAccountEmail on account change. |
| logout | destructive | Revokes at oauth2.googleapis.com/revoke, deletes the file, reports envTokenStillSet. |
Credential sources
OAuth client (shared by the whole line), priority order:
<PREFIX>_CLIENT_ID/<PREFIX>_CLIENT_SECRET(env override, e.g. CI)~/.config/mcp-google-auth/client.json— written byset_clientoptions.defaultClient— the built-in slot for the verified A1 client
The code exchange first runs without client_secret; if Google answers
"client_secret is missing", it retries with the secret when one is on file,
otherwise fails with set_client advice.
Tokens (per server), priority order:
<PREFIX>_REFRESH_TOKEN(+ client) — minted in memory, never written to disk;<PREFIX>_ACCESS_TOKENas a static testing alternative~/.config/mcp-google-<serverName>/credentials.json— written byfinish_login, re-read on every call (a mid-session login needs no restart)
Paths honor $XDG_CONFIG_HOME; on Windows the base is %APPDATA% (profile
ACLs replace the 0600/0700 modes, renames retry on antivirus EPERM). Writes
are atomic (temp file + rename); a broken file reads as "not connected".
Refresh: 60 s leeway, concurrent refreshes deduplicated, a rotated
refresh_token is persisted immediately.
Security invariants (each pinned by a test)
- No tool output or error ever contains
access_token/refresh_token/client_secret/id_token(sentinel grep over the full test transcript). - The PKCE verifier never leaves the process;
stateis compared exactly; the listener binds127.0.0.1only, is one-shot, and the pending login dies after 10 minutes. The success page is static — no reflection, no external resources. - Missing credentials raise
AuthRequiredErrorbefore any fetch; the message names both fixes (start_login, env variables). - OAuth failures map to actionable advice (
redirect_uri_mismatch,access_denied,accessNotConfigured,invalid_scope,invalid_grant,invalid_client,admin_policy_enforced). - A partial consent grant is stored and reported, never rejected; an account change revokes the old refresh token best-effort and names the previous account.
Development
npm install
npm run typecheck # types for src + tests
npm test # node:test, no external network (loopback only)
npm run build # emit dist/@modelcontextprotocol/sdk is a peer dependency — the host server owns
the SDK instance; only zod ships as a runtime dependency.
License
MIT
