Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -716,14 +716,34 @@ cp .env.oauth21 .env
| `get_gmail_messages_content_batch` | **Core** | Batch retrieve message content |
| `send_gmail_message` | **Core** | Send emails |
| `get_gmail_thread_content` | Extended | Get full thread content |
| `get_gmail_thread_metadata` | Extended | Get thread metadata (headers, labels, snippet) |
| `modify_gmail_message_labels` | Extended | Modify message labels |
| `modify_gmail_thread_labels` | Extended | Modify labels for all messages in a thread |
| `list_gmail_labels` | Extended | List available labels |
| `manage_gmail_label` | Extended | Create/update/delete labels |
| `draft_gmail_message` | Extended | Create drafts |
| `get_gmail_threads_content_batch` | Complete | Batch retrieve thread content |
| `get_gmail_threads_metadata_batch` | Complete | Batch retrieve thread metadata |
| `batch_modify_gmail_message_labels` | Complete | Batch modify labels |
| `batch_modify_gmail_thread_labels` | Complete | Batch modify labels for all messages in threads |
| `start_google_auth` | Complete | Initialize authentication |

**Gmail Message Format Options:**

Both `get_gmail_thread_content` and `get_gmail_threads_content_batch` support an optional `format` parameter (defaults to `"full"`):

| Format | Description |
|--------|-------------|
| `"minimal"` | Returns only the `id` and `threadId` of each message. |
| `"metadata"` | Returns message metadata such as headers, labels, and snippet, but *not* the full body. |
| `"full"` | Returns the full email message data, including headers and body (Base64 encoded). This is the default and most commonly used for reading messages. |

**Convenience Metadata Functions:**

For quick access to metadata only, use the dedicated metadata functions:
- `get_gmail_thread_metadata` - Equivalent to `get_gmail_thread_content` with `format="metadata"`
- `get_gmail_threads_metadata_batch` - Equivalent to `get_gmail_threads_content_batch` with `format="metadata"`

</td>
<td width="50%" valign="top">

Expand Down
14 changes: 13 additions & 1 deletion auth/credential_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,21 @@ def store_credential(self, user_email: str, credentials: Credentials) -> bool:
"""Store credentials to local JSON file."""
creds_path = self._get_credential_path(user_email)

# Preserve existing refresh token if new credentials don't have one
# This prevents losing refresh tokens during re-authorization flows
refresh_token_to_store = credentials.refresh_token
if not refresh_token_to_store:
try:
existing_creds = self.get_credential(user_email)
if existing_creds and existing_creds.refresh_token:
refresh_token_to_store = existing_creds.refresh_token
logger.info(f"Preserved existing refresh token for {user_email} in credential store")
except Exception as e:
logger.debug(f"Could not check existing credentials to preserve refresh token: {e}")

creds_data = {
"token": credentials.token,
"refresh_token": credentials.refresh_token,
"refresh_token": refresh_token_to_store,
"token_uri": credentials.token_uri,
"client_id": credentials.client_id,
"client_secret": credentials.client_secret,
Expand Down
41 changes: 34 additions & 7 deletions auth/google_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ async def start_auth_flow(
state=oauth_state,
)

auth_url, _ = flow.authorization_url(access_type="offline", prompt="consent")
auth_url, _ = flow.authorization_url(access_type="offline")

session_id = None
try:
Expand Down Expand Up @@ -486,16 +486,41 @@ def handle_auth_callback(
user_google_email = user_info["email"]
logger.info(f"Identified user_google_email: {user_google_email}")

# Save the credentials
# Preserve existing refresh token if new credentials don't have one
# Google often doesn't return refresh_token on re-authorization if one already exists
existing_credentials = None
credential_store = get_credential_store()
try:
existing_credentials = credential_store.get_credential(user_google_email)
except Exception as e:
logger.debug(f"Could not load existing credentials to preserve refresh token: {e}")

# Also check OAuth21SessionStore for existing refresh token
store = get_oauth21_session_store()
existing_session_creds = store.get_credentials(user_google_email)

# Preserve refresh token from existing credentials if new one is missing
preserved_refresh_token = credentials.refresh_token
if not preserved_refresh_token:
if existing_credentials and existing_credentials.refresh_token:
preserved_refresh_token = existing_credentials.refresh_token
logger.info(f"Preserved existing refresh token from credential store for {user_google_email}")
elif existing_session_creds and existing_session_creds.refresh_token:
preserved_refresh_token = existing_session_creds.refresh_token
logger.info(f"Preserved existing refresh token from session store for {user_google_email}")

# Update credentials object with preserved refresh token if we found one
if preserved_refresh_token and not credentials.refresh_token:
credentials.refresh_token = preserved_refresh_token

# Save the credentials
credential_store.store_credential(user_google_email, credentials)

# Always save to OAuth21SessionStore for centralized management
store = get_oauth21_session_store()
store.store_session(
user_email=user_google_email,
access_token=credentials.token,
refresh_token=credentials.refresh_token,
refresh_token=preserved_refresh_token or credentials.refresh_token,
token_uri=credentials.token_uri,
client_id=credentials.client_id,
client_secret=credentials.client_secret,
Expand All @@ -505,9 +530,8 @@ def handle_auth_callback(
issuer="https://accounts.google.com" # Add issuer for Google tokens
)

# If session_id is provided, also save to session cache for compatibility
if session_id:
save_credentials_to_session(session_id, credentials)
# Note: No need to call save_credentials_to_session() here as we've already
# saved to the OAuth21SessionStore above with the correct issuer and mcp_session_id

return user_google_email, credentials

Expand Down Expand Up @@ -570,6 +594,9 @@ def get_credentials(
user_email=user_email,
access_token=credentials.token,
refresh_token=credentials.refresh_token,
token_uri=credentials.token_uri,
client_id=credentials.client_id,
client_secret=credentials.client_secret,
scopes=credentials.scopes,
expiry=credentials.expiry,
mcp_session_id=session_id
Expand Down
11 changes: 10 additions & 1 deletion auth/oauth21_session_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,19 @@ def store_session(
issuer: Token issuer (e.g., "https://accounts.google.com")
"""
with self._lock:
# Preserve existing refresh token if new one is not provided
# This prevents losing refresh tokens during re-authorization flows
preserved_refresh_token = refresh_token
if not preserved_refresh_token:
existing_session = self._sessions.get(user_email)
if existing_session and existing_session.get("refresh_token"):
preserved_refresh_token = existing_session["refresh_token"]
logger.info(f"Preserved existing refresh token for {user_email} in session store")

normalized_expiry = _normalize_expiry_to_naive_utc(expiry)
session_info = {
"access_token": access_token,
"refresh_token": refresh_token,
"refresh_token": preserved_refresh_token,
"token_uri": token_uri,
"client_id": client_id,
"client_secret": client_secret,
Expand Down
4 changes: 4 additions & 0 deletions core/tool_tiers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ gmail:

extended:
- get_gmail_thread_content
- get_gmail_thread_metadata
- modify_gmail_message_labels
- modify_gmail_thread_labels
- list_gmail_labels
- manage_gmail_label
- draft_gmail_message

complete:
- get_gmail_threads_content_batch
- get_gmail_threads_metadata_batch
- batch_modify_gmail_message_labels
- batch_modify_gmail_thread_labels
- start_google_auth

drive:
Expand Down
Loading
Loading