When `create_oauth_flow()` is called without an explicit `code_verifier`
(i.e. during the initial auth flow in `start_auth_flow()`), the function
never sets `autogenerate_code_verifier=True` on the Flow constructor.
oauthlib 3.2+ automatically adds `code_challenge` to the authorization
URL at the session level, so Google expects a matching `code_verifier`
during the token exchange. However, since `Flow.code_verifier` remains
`None`, that `None` gets stored in the session store and later passed
back during the callback — causing Google to reject the token exchange
with `(invalid_grant) Missing code verifier`.
The fix adds `autogenerate_code_verifier=True` in the else branch so
the Flow object generates and exposes a proper PKCE code verifier that
gets stored and reused during the callback token exchange.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The list_spaces tool was using chat.messages.readonly which is overly
broad for simply enumerating available spaces. This adds the
chat.spaces.readonly scope and uses it for list_spaces, following the
principle of least privilege.
Closes#479
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LocalDirectoryCredentialStore.list_users() enumerates all .json files
in the credentials directory, but oauth_states.json (written by
PersistentOAuthStateStore) is not a user credential file. In
single-user mode, this file can be picked up first alphabetically,
causing a TypeError when accessing credentials.scopes (None) since
the state file has no scopes field.
Filter out known non-credential files and filenames without '@' to
ensure only actual user credential files are returned.
The store_session call in the OAuth 2.1 credential refresh path (get_credentials)
omits token_uri, client_id, client_secret, and issuer. These are stored as None,
causing subsequent refresh attempts to fail and forcing full re-authentication.
The correct pattern already exists in three other store_session calls in the same
file (lines 151, 522, 750) — this aligns the refresh path to match.
Several docs tools (search_docs, get_doc_content, list_docs_in_folder,
export_doc_to_pdf) and sheets tools (list_spreadsheets) internally use
the Google Drive API but only receive docs/sheets-specific OAuth scopes
when configured with `--tools docs sheets` (without `drive`).
This adds the minimal required Drive scopes as cross-service dependencies:
- docs: drive.readonly (metadata queries) + drive.file (PDF export)
- sheets: drive.readonly (spreadsheet listing)
This follows the existing pattern where appscript already includes
DRIVE_FILE_SCOPE for its Drive API dependency.
The alternative workaround of adding `--tools drive` exposes 14
full-access Drive tools which is undesirable from a security perspective.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
In external OAuth 2.1 mode, skip session storage entirely.
The access token arrives fresh with every request from the external
provider, there's no refresh_token, and the mcp_session_id is
ephemeral (new UUID per request in stateless mode). Storing these
transient tokens creates unbounded dict growth (memory leak) with
entries that are never cleaned up or reused.
Credit to @ljagiello in PR #383
Co-authored-by: lukasz@jagiello.org