Arun Raghav commited on
Commit
627b659
Β·
1 Parent(s): 2312d97

google auth done

Browse files
agentic_implementation/README_OAuth.md ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gmail MCP Server with OAuth Authentication
2
+
3
+ This is an enhanced version of the Gmail MCP (Model Context Protocol) server that uses **OAuth 2.0 authentication** instead of requiring users to provide email credentials for each query.
4
+
5
+ ## πŸš€ Key Features
6
+
7
+ - **OAuth 2.0 Authentication**: Secure authentication flow using Google's OAuth system
8
+ - **One-time Setup**: Authenticate once, use anywhere
9
+ - **Automatic Token Refresh**: Handles token expiration automatically
10
+ - **Encrypted Storage**: Credentials are encrypted and stored securely
11
+ - **No More Password Sharing**: No need to provide email/password to Claude
12
+
13
+ ## πŸ“‹ Prerequisites
14
+
15
+ 1. **Google Account**: You need a Gmail account
16
+ 2. **Google Cloud Project**: Free to create
17
+ 3. **Python 3.8+**: Required for running the server
18
+
19
+ ## πŸ› οΈ Setup Instructions
20
+
21
+ ### Step 1: Install Dependencies
22
+
23
+ ```bash
24
+ pip install -r requirements_oauth.txt
25
+ ```
26
+
27
+ ### Step 2: Run the Interactive Setup
28
+
29
+ The setup script will guide you through the entire process:
30
+
31
+ ```bash
32
+ python setup_oauth.py
33
+ ```
34
+
35
+ This will walk you through:
36
+ 1. Creating a Google Cloud project
37
+ 2. Enabling the Gmail API
38
+ 3. Setting up OAuth consent screen
39
+ 4. Creating OAuth credentials
40
+ 5. Testing the authentication flow
41
+
42
+ ### Step 3: Start the MCP Server
43
+
44
+ ```bash
45
+ python email_mcp_server_oauth.py
46
+ ```
47
+
48
+ The server will start and show you:
49
+ - Authentication status
50
+ - MCP endpoint URL
51
+ - Web interface URL
52
+
53
+ ## πŸ”§ Claude Desktop Configuration
54
+
55
+ Add this configuration to your Claude Desktop MCP settings:
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "gmail-oauth": {
61
+ "command": "npx",
62
+ "args": [
63
+ "mcp-remote",
64
+ "http://localhost:7860/gradio_api/mcp/sse"
65
+ ]
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## πŸ” Available Tools
72
+
73
+ ### 1. search_emails
74
+ Search your emails using natural language queries - **no credentials needed!**
75
+
76
+ **Parameters:**
77
+ - `query`: Natural language query (e.g., "show me emails from amazon last week")
78
+
79
+ **Example Usage in Claude:**
80
+ > "Can you search my emails for messages from Swiggy in the last week?"
81
+
82
+ ### 2. get_email_details
83
+ Get full details of a specific email by message ID.
84
+
85
+ **Parameters:**
86
+ - `message_id`: Message ID from search results
87
+
88
+ ### 3. analyze_email_patterns
89
+ Analyze email patterns from a specific sender over time.
90
+
91
+ **Parameters:**
92
+ - `sender_keyword`: Sender to analyze (e.g., "amazon", "google")
93
+ - `days_back`: Number of days to analyze (default: "30")
94
+
95
+ ### 4. authenticate_user
96
+ Trigger the OAuth authentication flow from Claude Desktop.
97
+
98
+ **Parameters:** None
99
+
100
+ ### 5. get_authentication_status
101
+ Check current authentication status.
102
+
103
+ **Parameters:** None
104
+
105
+ ## πŸ” Security Features
106
+
107
+ ### Encrypted Storage
108
+ - All credentials are encrypted using Fernet encryption
109
+ - Encryption keys are stored securely with proper permissions
110
+ - No plaintext credentials are ever stored
111
+
112
+ ### OAuth Benefits
113
+ - No need to share Gmail passwords
114
+ - Granular permission control
115
+ - Easy revocation from Google Account settings
116
+ - Automatic token refresh
117
+
118
+ ### Local Storage
119
+ - All data stored locally on your machine
120
+ - No cloud storage of credentials
121
+ - You maintain full control
122
+
123
+ ## πŸ”§ Advanced Usage
124
+
125
+ ### Command Line Tools
126
+
127
+ Check authentication status:
128
+ ```bash
129
+ python setup_oauth.py --status
130
+ ```
131
+
132
+ Re-authenticate:
133
+ ```bash
134
+ python setup_oauth.py --auth
135
+ ```
136
+
137
+ Clear stored credentials:
138
+ ```bash
139
+ python setup_oauth.py --clear
140
+ ```
141
+
142
+ Show help:
143
+ ```bash
144
+ python setup_oauth.py --help
145
+ ```
146
+
147
+ ### Web Interface
148
+
149
+ When the server is running, you can access the web interface at:
150
+ ```
151
+ http://localhost:7860
152
+ ```
153
+
154
+ Use this interface to:
155
+ - Check authentication status
156
+ - Trigger authentication flow
157
+ - Test email search functionality
158
+
159
+ ## πŸ†š Comparison: OAuth vs App Passwords
160
+
161
+ | Feature | App Password (Old) | OAuth (New) |
162
+ |---------|-------------------|-------------|
163
+ | **Setup Complexity** | Simple | One-time setup required |
164
+ | **Security** | Share app password | No password sharing |
165
+ | **User Experience** | Enter credentials each time | Authenticate once |
166
+ | **Revocation** | Change app password | Revoke from Google Account |
167
+ | **Token Management** | Manual | Automatic refresh |
168
+ | **Scope Control** | Full Gmail access | Granular permissions |
169
+
170
+ ## πŸ› Troubleshooting
171
+
172
+ ### Authentication Issues
173
+
174
+ **"OAuth not configured" error:**
175
+ ```bash
176
+ python setup_oauth.py
177
+ ```
178
+
179
+ **"Not authenticated" error:**
180
+ ```bash
181
+ python setup_oauth.py --auth
182
+ ```
183
+
184
+ **Authentication timeout:**
185
+ - Check if port 8080 is available
186
+ - Try disabling firewall temporarily
187
+ - Ensure browser can access localhost:8080
188
+
189
+ ### Common Issues
190
+
191
+ **"No module named 'google.auth'" error:**
192
+ ```bash
193
+ pip install -r requirements_oauth.txt
194
+ ```
195
+
196
+ **"Permission denied" on credential files:**
197
+ ```bash
198
+ # Check permissions
199
+ ls -la ~/.mailquery_oauth/
200
+ # Should show restricted permissions (600/700)
201
+ ```
202
+
203
+ **Browser doesn't open:**
204
+ - Copy the authorization URL manually
205
+ - Paste it in your browser
206
+ - Complete the flow manually
207
+
208
+ ### Getting Help
209
+
210
+ 1. Check authentication status: `python setup_oauth.py --status`
211
+ 2. Review server logs for detailed error messages
212
+ 3. Ensure Google Cloud project is properly configured
213
+ 4. Verify OAuth consent screen is set up correctly
214
+
215
+ ## πŸ“ File Structure
216
+
217
+ ```
218
+ ~/.mailquery_oauth/
219
+ β”œβ”€β”€ client_secret.json # OAuth client configuration
220
+ β”œβ”€β”€ token.pickle # Encrypted access/refresh tokens
221
+ └── key.key # Encryption key (secure permissions)
222
+ ```
223
+
224
+ ## πŸ”„ Migration from App Password Version
225
+
226
+ If you're migrating from the app password version:
227
+
228
+ 1. Run the new OAuth setup: `python setup_oauth.py`
229
+ 2. Update your Claude Desktop configuration to use the new server
230
+ 3. The old environment variables (EMAIL_ID, APP_PASSWORD) are no longer needed
231
+
232
+ ## πŸ“ž Support
233
+
234
+ For issues or questions:
235
+ 1. Check the troubleshooting section above
236
+ 2. Review the setup script output for specific guidance
237
+ 3. Ensure all prerequisites are met
238
+ 4. Verify Google Cloud project configuration
239
+
240
+ ## 🎯 Example Queries for Claude
241
+
242
+ Once set up, you can ask Claude:
243
+
244
+ - "Search my emails for messages from Amazon in the last month"
245
+ - "Show me emails from my bank from last week"
246
+ - "Analyze my LinkedIn email patterns over the last 60 days"
247
+ - "Find emails from Swiggy today"
248
+ - "Get details of the email with ID xyz123"
249
+
250
+ Claude will automatically use the OAuth-authenticated tools without asking for credentials!
251
+
252
+ ## πŸ”’ Privacy & Data
253
+
254
+ - **No data leaves your machine**: All processing happens locally
255
+ - **Google only provides**: Access to your Gmail via official APIs
256
+ - **We store**: Encrypted authentication tokens only
257
+ - **We never store**: Email content, passwords, or personal data
258
+ - **You control**: Access can be revoked anytime from Google Account settings
agentic_implementation/email_mcp_server_oauth.py ADDED
@@ -0,0 +1,545 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import json
3
+ import os
4
+ from typing import Dict, List
5
+ from datetime import datetime, timedelta
6
+ from dotenv import load_dotenv
7
+
8
+ # Import OAuth-enabled modules
9
+ from tools import extract_query_info, analyze_emails
10
+ from gmail_api_scraper import gmail_scraper
11
+ from oauth_manager import oauth_manager
12
+ from logger import logger
13
+
14
+ load_dotenv()
15
+
16
+ def check_authentication() -> tuple[bool, str]:
17
+ """Check if user is authenticated and return status info"""
18
+ if oauth_manager.is_authenticated():
19
+ user_email = oauth_manager.get_user_email()
20
+ return True, user_email or "authenticated"
21
+ return False, "not authenticated"
22
+
23
+ def authenticate_user() -> str:
24
+ """
25
+ Start OAuth authentication flow for Gmail access.
26
+ Opens a browser window for user to authenticate with Google.
27
+
28
+ Returns:
29
+ str: JSON string containing authentication result
30
+ """
31
+ try:
32
+ logger.info("Starting OAuth authentication flow...")
33
+
34
+ # Check if OAuth is configured
35
+ if not oauth_manager.client_secrets_file.exists():
36
+ return json.dumps({
37
+ "error": "OAuth not configured",
38
+ "message": "Please run 'python setup_oauth.py' first to configure OAuth credentials.",
39
+ "success": False
40
+ }, indent=2)
41
+
42
+ # Start authentication
43
+ success = oauth_manager.authenticate_interactive()
44
+
45
+ if success:
46
+ user_email = oauth_manager.get_current_account()
47
+ result = {
48
+ "success": True,
49
+ "message": "Authentication successful! You can now use the email tools.",
50
+ "user_email": user_email,
51
+ "instructions": [
52
+ "Authentication completed successfully",
53
+ "You can now search emails, get email details, and analyze patterns",
54
+ f"Currently authenticated as: {user_email}"
55
+ ]
56
+ }
57
+ else:
58
+ result = {
59
+ "success": False,
60
+ "error": "Authentication failed",
61
+ "message": "Please try again or check your internet connection.",
62
+ "instructions": [
63
+ "Make sure you have internet connection",
64
+ "Ensure you complete the authentication in the browser",
65
+ "Try running 'python setup_oauth.py' if problems persist"
66
+ ]
67
+ }
68
+
69
+ return json.dumps(result, indent=2)
70
+
71
+ except Exception as e:
72
+ logger.error("Error in authenticate_user: %s", e)
73
+ error_result = {
74
+ "success": False,
75
+ "error": str(e),
76
+ "message": "Authentication failed due to an error."
77
+ }
78
+ return json.dumps(error_result, indent=2)
79
+
80
+ def switch_account(target_email: str) -> str:
81
+ """
82
+ Switch to a different authenticated Gmail account.
83
+
84
+ Args:
85
+ target_email (str): Email address to switch to
86
+
87
+ Returns:
88
+ str: JSON string containing switch result
89
+ """
90
+ try:
91
+ logger.info("Switching to account: %s", target_email)
92
+
93
+ # Check if target account is authenticated
94
+ if not oauth_manager.is_authenticated(target_email):
95
+ return json.dumps({
96
+ "error": "Account not authenticated",
97
+ "message": f"Account '{target_email}' is not authenticated. Please authenticate first.",
98
+ "target_email": target_email,
99
+ "authenticated_accounts": list(oauth_manager.list_accounts().keys())
100
+ }, indent=2)
101
+
102
+ # Switch account
103
+ success = oauth_manager.switch_account(target_email)
104
+
105
+ if success:
106
+ result = {
107
+ "success": True,
108
+ "message": f"Successfully switched to account: {target_email}",
109
+ "current_account": oauth_manager.get_current_account(),
110
+ "previous_account": None # Could track this if needed
111
+ }
112
+ else:
113
+ result = {
114
+ "success": False,
115
+ "error": "Failed to switch account",
116
+ "message": f"Could not switch to account: {target_email}",
117
+ "current_account": oauth_manager.get_current_account()
118
+ }
119
+
120
+ return json.dumps(result, indent=2)
121
+
122
+ except Exception as e:
123
+ logger.error("Error switching account: %s", e)
124
+ error_result = {
125
+ "success": False,
126
+ "error": str(e),
127
+ "message": f"Failed to switch to account: {target_email}"
128
+ }
129
+ return json.dumps(error_result, indent=2)
130
+
131
+ def list_accounts() -> str:
132
+ """
133
+ List all authenticated Gmail accounts and their status.
134
+
135
+ Returns:
136
+ str: JSON string containing all accounts and their authentication status
137
+ """
138
+ try:
139
+ logger.info("Listing all accounts")
140
+
141
+ accounts = oauth_manager.list_accounts()
142
+ current_account = oauth_manager.get_current_account()
143
+
144
+ result = {
145
+ "accounts": accounts,
146
+ "current_account": current_account,
147
+ "total_accounts": len(accounts),
148
+ "authenticated_accounts": [email for email, is_auth in accounts.items() if is_auth],
149
+ "message": f"Found {len(accounts)} stored accounts, currently using: {current_account or 'None'}"
150
+ }
151
+
152
+ return json.dumps(result, indent=2)
153
+
154
+ except Exception as e:
155
+ logger.error("Error listing accounts: %s", e)
156
+ error_result = {
157
+ "error": str(e),
158
+ "message": "Failed to list accounts"
159
+ }
160
+ return json.dumps(error_result, indent=2)
161
+
162
+ def remove_account(email_to_remove: str) -> str:
163
+ """
164
+ Remove an authenticated Gmail account and its stored credentials.
165
+
166
+ Args:
167
+ email_to_remove (str): Email address to remove
168
+
169
+ Returns:
170
+ str: JSON string containing removal result
171
+ """
172
+ try:
173
+ logger.info("Removing account: %s", email_to_remove)
174
+
175
+ # Check if account exists
176
+ accounts = oauth_manager.list_accounts()
177
+ if email_to_remove not in accounts:
178
+ return json.dumps({
179
+ "error": "Account not found",
180
+ "message": f"Account '{email_to_remove}' not found in stored accounts.",
181
+ "available_accounts": list(accounts.keys())
182
+ }, indent=2)
183
+
184
+ # Remove account
185
+ oauth_manager.remove_account(email_to_remove)
186
+
187
+ result = {
188
+ "success": True,
189
+ "message": f"Successfully removed account: {email_to_remove}",
190
+ "removed_account": email_to_remove,
191
+ "current_account": oauth_manager.get_current_account(),
192
+ "remaining_accounts": list(oauth_manager.list_accounts().keys())
193
+ }
194
+
195
+ return json.dumps(result, indent=2)
196
+
197
+ except Exception as e:
198
+ logger.error("Error removing account: %s", e)
199
+ error_result = {
200
+ "success": False,
201
+ "error": str(e),
202
+ "message": f"Failed to remove account: {email_to_remove}"
203
+ }
204
+ return json.dumps(error_result, indent=2)
205
+
206
+ def search_emails(query: str) -> str:
207
+ """
208
+ Search for emails based on a natural language query using OAuth authentication.
209
+
210
+ Args:
211
+ query (str): Natural language query (e.g., "show me mails from swiggy last week")
212
+
213
+ Returns:
214
+ str: JSON string containing email search results and analysis
215
+ """
216
+ try:
217
+ logger.info("OAuth Email search tool called with query: %s", query)
218
+
219
+ # Check authentication
220
+ is_auth, auth_info = check_authentication()
221
+ if not is_auth:
222
+ return json.dumps({
223
+ "error": "Not authenticated",
224
+ "message": "Please authenticate first using the authenticate_user tool or run 'python setup_oauth.py'",
225
+ "auth_status": auth_info
226
+ }, indent=2)
227
+
228
+ # Extract sender keyword and date range from query
229
+ query_info = extract_query_info(query)
230
+ sender_keyword = query_info.get("sender_keyword", "")
231
+ start_date = query_info.get("start_date")
232
+ end_date = query_info.get("end_date")
233
+
234
+ logger.info(f"Searching for emails with keyword '{sender_keyword}' between {start_date} and {end_date}")
235
+
236
+ # Use Gmail API scraper with OAuth
237
+ full_emails = gmail_scraper.search_emails(sender_keyword, start_date, end_date)
238
+
239
+ if not full_emails:
240
+ result = {
241
+ "query_info": query_info,
242
+ "email_summary": [],
243
+ "analysis": {"summary": f"No emails found for '{sender_keyword}' in the specified date range.", "insights": []},
244
+ "email_count": 0,
245
+ "user_email": auth_info
246
+ }
247
+ return json.dumps(result, indent=2)
248
+
249
+ # Create summary version without full content
250
+ email_summary = []
251
+ for email in full_emails:
252
+ summary_email = {
253
+ "date": email.get("date"),
254
+ "time": email.get("time"),
255
+ "subject": email.get("subject"),
256
+ "from": email.get("from", "Unknown Sender"),
257
+ "message_id": email.get("message_id"),
258
+ "gmail_id": email.get("gmail_id")
259
+ }
260
+ email_summary.append(summary_email)
261
+
262
+ # Auto-analyze the emails for insights
263
+ analysis = analyze_emails(full_emails)
264
+
265
+ # Return summary info with analysis
266
+ result = {
267
+ "query_info": query_info,
268
+ "email_summary": email_summary,
269
+ "analysis": analysis,
270
+ "email_count": len(full_emails),
271
+ "user_email": auth_info
272
+ }
273
+
274
+ return json.dumps(result, indent=2)
275
+
276
+ except Exception as e:
277
+ logger.error("Error in search_emails: %s", e)
278
+ error_result = {
279
+ "error": str(e),
280
+ "query": query,
281
+ "message": "Failed to search emails. Please check your authentication and try again."
282
+ }
283
+ return json.dumps(error_result, indent=2)
284
+
285
+ def get_email_details(message_id: str) -> str:
286
+ """
287
+ Get full details of a specific email by its message ID using OAuth authentication.
288
+
289
+ Args:
290
+ message_id (str): The message ID of the email to retrieve
291
+
292
+ Returns:
293
+ str: JSON string containing the full email details
294
+ """
295
+ try:
296
+ logger.info("Getting email details for message_id: %s", message_id)
297
+
298
+ # Check authentication
299
+ is_auth, auth_info = check_authentication()
300
+ if not is_auth:
301
+ return json.dumps({
302
+ "error": "Not authenticated",
303
+ "message": "Please authenticate first using the authenticate_user tool or run 'python setup_oauth.py'",
304
+ "auth_status": auth_info
305
+ }, indent=2)
306
+
307
+ # Get email using Gmail API
308
+ email = gmail_scraper.get_email_by_id(message_id)
309
+
310
+ if email:
311
+ email["user_email"] = auth_info
312
+ return json.dumps(email, indent=2)
313
+ else:
314
+ error_result = {
315
+ "error": f"No email found with message_id '{message_id}'",
316
+ "message": "Email may not exist or you may not have access to it.",
317
+ "user_email": auth_info
318
+ }
319
+ return json.dumps(error_result, indent=2)
320
+
321
+ except Exception as e:
322
+ logger.error("Error in get_email_details: %s", e)
323
+ error_result = {
324
+ "error": str(e),
325
+ "message_id": message_id,
326
+ "message": "Failed to retrieve email details."
327
+ }
328
+ return json.dumps(error_result, indent=2)
329
+
330
+ def analyze_email_patterns(sender_keyword: str, days_back: str = "30") -> str:
331
+ """
332
+ Analyze email patterns from a specific sender over a given time period using OAuth authentication.
333
+
334
+ Args:
335
+ sender_keyword (str): The sender/company keyword to analyze (e.g., "amazon", "google")
336
+ days_back (str): Number of days to look back (default: "30")
337
+
338
+ Returns:
339
+ str: JSON string containing email pattern analysis
340
+ """
341
+ try:
342
+ logger.info("Analyzing email patterns for sender: %s, days_back: %s", sender_keyword, days_back)
343
+
344
+ # Check authentication
345
+ is_auth, auth_info = check_authentication()
346
+ if not is_auth:
347
+ return json.dumps({
348
+ "error": "Not authenticated",
349
+ "message": "Please authenticate first using the authenticate_user tool or run 'python setup_oauth.py'",
350
+ "auth_status": auth_info
351
+ }, indent=2)
352
+
353
+ # Calculate date range
354
+ days_int = int(days_back)
355
+ end_date = datetime.today()
356
+ start_date = end_date - timedelta(days=days_int)
357
+
358
+ start_date_str = start_date.strftime("%d-%b-%Y")
359
+ end_date_str = end_date.strftime("%d-%b-%Y")
360
+
361
+ # Search for emails using Gmail API
362
+ full_emails = gmail_scraper.search_emails(sender_keyword, start_date_str, end_date_str)
363
+
364
+ if not full_emails:
365
+ result = {
366
+ "sender_keyword": sender_keyword,
367
+ "date_range": f"{start_date_str} to {end_date_str}",
368
+ "analysis": {"summary": f"No emails found from '{sender_keyword}' in the last {days_back} days.", "insights": []},
369
+ "email_count": 0,
370
+ "user_email": auth_info
371
+ }
372
+ return json.dumps(result, indent=2)
373
+
374
+ # Analyze the emails
375
+ analysis = analyze_emails(full_emails)
376
+
377
+ result = {
378
+ "sender_keyword": sender_keyword,
379
+ "date_range": f"{start_date_str} to {end_date_str}",
380
+ "analysis": analysis,
381
+ "email_count": len(full_emails),
382
+ "user_email": auth_info
383
+ }
384
+
385
+ return json.dumps(result, indent=2)
386
+
387
+ except Exception as e:
388
+ logger.error("Error in analyze_email_patterns: %s", e)
389
+ error_result = {
390
+ "error": str(e),
391
+ "sender_keyword": sender_keyword,
392
+ "message": "Failed to analyze email patterns."
393
+ }
394
+ return json.dumps(error_result, indent=2)
395
+
396
+ def get_authentication_status() -> str:
397
+ """
398
+ Get current authentication status and account information.
399
+
400
+ Returns:
401
+ str: JSON string containing authentication status
402
+ """
403
+ try:
404
+ current_account = oauth_manager.get_current_account()
405
+ is_auth = oauth_manager.is_authenticated() if current_account else False
406
+ all_accounts = oauth_manager.list_accounts()
407
+
408
+ result = {
409
+ "authenticated": is_auth,
410
+ "current_account": current_account,
411
+ "status": "authenticated" if is_auth else "not_authenticated",
412
+ "message": f"Current account: {current_account}" if is_auth else "No account selected or not authenticated",
413
+ "all_accounts": all_accounts,
414
+ "total_accounts": len(all_accounts),
415
+ "authenticated_accounts": [email for email, auth in all_accounts.items() if auth]
416
+ }
417
+
418
+ if not is_auth and not oauth_manager.client_secrets_file.exists():
419
+ result["setup_required"] = True
420
+ result["message"] = "OAuth not configured. Please run 'python setup_oauth.py' first."
421
+ elif not is_auth and current_account:
422
+ result["message"] = f"Account {current_account} needs re-authentication"
423
+ elif not current_account and all_accounts:
424
+ result["message"] = "Accounts available but none selected. Use switch_account to select one."
425
+
426
+ return json.dumps(result, indent=2)
427
+
428
+ except Exception as e:
429
+ logger.error("Error checking authentication status: %s", e)
430
+ return json.dumps({
431
+ "error": str(e),
432
+ "message": "Failed to check authentication status"
433
+ }, indent=2)
434
+
435
+ # Create Gradio interfaces
436
+ search_interface = gr.Interface(
437
+ fn=search_emails,
438
+ inputs=[
439
+ gr.Textbox(label="Query", placeholder="Show me emails from amazon last week")
440
+ ],
441
+ outputs=gr.Textbox(label="Search Results", lines=20),
442
+ title="Email Search (OAuth)",
443
+ description="Search your emails using natural language queries with OAuth authentication"
444
+ )
445
+
446
+ details_interface = gr.Interface(
447
+ fn=get_email_details,
448
+ inputs=[
449
+ gr.Textbox(label="Message ID", placeholder="Email message ID from search results")
450
+ ],
451
+ outputs=gr.Textbox(label="Email Details", lines=20),
452
+ title="Email Details (OAuth)",
453
+ description="Get full details of a specific email by message ID with OAuth authentication"
454
+ )
455
+
456
+ analysis_interface = gr.Interface(
457
+ fn=analyze_email_patterns,
458
+ inputs=[
459
+ gr.Textbox(label="Sender Keyword", placeholder="amazon, google, linkedin, etc."),
460
+ gr.Textbox(label="Days Back", value="30", placeholder="Number of days to analyze")
461
+ ],
462
+ outputs=gr.Textbox(label="Analysis Results", lines=20),
463
+ title="Email Pattern Analysis (OAuth)",
464
+ description="Analyze email patterns from a specific sender over time with OAuth authentication"
465
+ )
466
+
467
+ auth_interface = gr.Interface(
468
+ fn=authenticate_user,
469
+ inputs=[],
470
+ outputs=gr.Textbox(label="Authentication Result", lines=10),
471
+ title="Authenticate with Gmail",
472
+ description="Click Submit to start OAuth authentication flow with Gmail"
473
+ )
474
+
475
+ status_interface = gr.Interface(
476
+ fn=get_authentication_status,
477
+ inputs=[],
478
+ outputs=gr.Textbox(label="Authentication Status", lines=15),
479
+ title="Authentication Status",
480
+ description="Check current authentication status and view all accounts"
481
+ )
482
+
483
+ switch_interface = gr.Interface(
484
+ fn=switch_account,
485
+ inputs=[
486
+ gr.Textbox(label="Target Email", placeholder="[email protected]")
487
+ ],
488
+ outputs=gr.Textbox(label="Switch Result", lines=10),
489
+ title="Switch Account",
490
+ description="Switch to a different authenticated Gmail account"
491
+ )
492
+
493
+ accounts_interface = gr.Interface(
494
+ fn=list_accounts,
495
+ inputs=[],
496
+ outputs=gr.Textbox(label="Accounts List", lines=15),
497
+ title="List All Accounts",
498
+ description="View all authenticated Gmail accounts and their status"
499
+ )
500
+
501
+ remove_interface = gr.Interface(
502
+ fn=remove_account,
503
+ inputs=[
504
+ gr.Textbox(label="Email to Remove", placeholder="[email protected]")
505
+ ],
506
+ outputs=gr.Textbox(label="Removal Result", lines=10),
507
+ title="Remove Account",
508
+ description="Remove an authenticated Gmail account and its credentials"
509
+ )
510
+
511
+ # Combine interfaces into a tabbed interface
512
+ demo = gr.TabbedInterface(
513
+ [auth_interface, status_interface, accounts_interface, switch_interface, remove_interface, search_interface, details_interface, analysis_interface],
514
+ ["πŸ” Authenticate", "πŸ“Š Status", "πŸ‘₯ All Accounts", "πŸ”„ Switch Account", "πŸ—‘οΈ Remove Account", "πŸ“§ Email Search", "πŸ“„ Email Details", "πŸ“ˆ Pattern Analysis"],
515
+ title="πŸ“§ Gmail Assistant MCP Server (Multi-Account OAuth)"
516
+ )
517
+
518
+ if __name__ == "__main__":
519
+ # Set environment variable to enable MCP server
520
+ import os
521
+ os.environ["GRADIO_MCP_SERVER"] = "True"
522
+
523
+ # Check authentication status on startup
524
+ current_account = oauth_manager.get_current_account()
525
+ all_accounts = oauth_manager.list_accounts()
526
+
527
+ if current_account and oauth_manager.is_authenticated():
528
+ print(f"βœ… Currently authenticated as: {current_account}")
529
+ if len(all_accounts) > 1:
530
+ print(f"πŸ“± {len(all_accounts)} total accounts available: {list(all_accounts.keys())}")
531
+ elif all_accounts:
532
+ print(f"πŸ“± {len(all_accounts)} stored accounts found: {list(all_accounts.keys())}")
533
+ print("⚠️ No current account selected. Use the web interface or Claude to switch accounts.")
534
+ else:
535
+ print("❌ No authenticated accounts. Users will need to authenticate through the web interface.")
536
+ print("πŸ’‘ Or run 'python setup_oauth.py' for initial setup.")
537
+
538
+ # Launch the server
539
+ demo.launch(share=False)
540
+
541
+ print("\nπŸš€ MCP Server is running!")
542
+ print("πŸ“ MCP Endpoint: http://localhost:7860/gradio_api/mcp/sse")
543
+ print("πŸ“– Copy this URL to your Claude Desktop MCP configuration")
544
+ print("\nπŸ”— Web Interface: http://localhost:7860")
545
+ print("πŸ“ Use the web interface to authenticate and test the tools")
agentic_implementation/gmail_api_scraper.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gmail API-based Email Scraper with OAuth Authentication
4
+ """
5
+
6
+ import base64
7
+ import re
8
+ from datetime import datetime, timedelta
9
+ from typing import List, Dict, Optional
10
+ from email.mime.text import MIMEText
11
+ import googleapiclient.errors
12
+ from oauth_manager import oauth_manager
13
+ from logger import logger
14
+
15
+ class GmailAPIScraper:
16
+ """Gmail API-based email scraper using OAuth authentication"""
17
+
18
+ def __init__(self):
19
+ """Initialize the Gmail API scraper"""
20
+ self.oauth_manager = oauth_manager
21
+
22
+ def _parse_date_string(self, date_str: str) -> datetime:
23
+ """Parse date string in DD-MMM-YYYY format to datetime object"""
24
+ try:
25
+ return datetime.strptime(date_str, "%d-%b-%Y")
26
+ except ValueError:
27
+ raise ValueError(f"Invalid date format: {date_str}. Expected DD-MMM-YYYY")
28
+
29
+ def _format_date_for_query(self, date_obj: datetime) -> str:
30
+ """Format datetime object for Gmail API query"""
31
+ return date_obj.strftime("%Y/%m/%d")
32
+
33
+ def _decode_message_part(self, part: Dict) -> str:
34
+ """Decode message part content"""
35
+ data = part.get('body', {}).get('data', '')
36
+ if data:
37
+ # Decode base64url
38
+ data += '=' * (4 - len(data) % 4) # Add padding if needed
39
+ decoded_bytes = base64.urlsafe_b64decode(data)
40
+ try:
41
+ return decoded_bytes.decode('utf-8')
42
+ except UnicodeDecodeError:
43
+ return decoded_bytes.decode('utf-8', errors='ignore')
44
+ return ''
45
+
46
+ def _extract_email_content(self, message: Dict) -> str:
47
+ """Extract readable content from Gmail API message"""
48
+ content = ""
49
+
50
+ if 'payload' not in message:
51
+ return content
52
+
53
+ payload = message['payload']
54
+
55
+ # Handle multipart messages
56
+ if 'parts' in payload:
57
+ for part in payload['parts']:
58
+ mime_type = part.get('mimeType', '')
59
+
60
+ if mime_type == 'text/plain':
61
+ content += self._decode_message_part(part)
62
+ elif mime_type == 'text/html':
63
+ html_content = self._decode_message_part(part)
64
+ # Simple HTML tag removal
65
+ clean_text = re.sub(r'<[^>]+>', '', html_content)
66
+ content += clean_text
67
+ elif mime_type.startswith('multipart/'):
68
+ # Handle nested multipart
69
+ if 'parts' in part:
70
+ for nested_part in part['parts']:
71
+ nested_mime = nested_part.get('mimeType', '')
72
+ if nested_mime == 'text/plain':
73
+ content += self._decode_message_part(nested_part)
74
+ else:
75
+ # Handle single part messages
76
+ mime_type = payload.get('mimeType', '')
77
+ if mime_type in ['text/plain', 'text/html']:
78
+ raw_content = self._decode_message_part(payload)
79
+ if mime_type == 'text/html':
80
+ # Simple HTML tag removal
81
+ content = re.sub(r'<[^>]+>', '', raw_content)
82
+ else:
83
+ content = raw_content
84
+
85
+ return content.strip()
86
+
87
+ def _get_header_value(self, headers: List[Dict], name: str) -> str:
88
+ """Get header value by name"""
89
+ for header in headers:
90
+ if header.get('name', '').lower() == name.lower():
91
+ return header.get('value', '')
92
+ return ''
93
+
94
+ def _parse_email_message(self, message: Dict) -> Dict:
95
+ """Parse Gmail API message into structured format"""
96
+ headers = message.get('payload', {}).get('headers', [])
97
+
98
+ # Extract headers
99
+ subject = self._get_header_value(headers, 'Subject') or 'No Subject'
100
+ from_header = self._get_header_value(headers, 'From') or 'Unknown Sender'
101
+ date_header = self._get_header_value(headers, 'Date')
102
+ message_id = self._get_header_value(headers, 'Message-ID') or message.get('id', '')
103
+
104
+ # Parse date
105
+ email_date = datetime.now().strftime("%d-%b-%Y")
106
+ email_time = "00:00:00"
107
+
108
+ if date_header:
109
+ try:
110
+ # Parse RFC 2822 date format
111
+ from email.utils import parsedate_to_datetime
112
+ dt_obj = parsedate_to_datetime(date_header)
113
+ # Convert to IST (Indian Standard Time)
114
+ from zoneinfo import ZoneInfo
115
+ ist_dt = dt_obj.astimezone(ZoneInfo("Asia/Kolkata"))
116
+ email_date = ist_dt.strftime("%d-%b-%Y")
117
+ email_time = ist_dt.strftime("%H:%M:%S")
118
+ except Exception as e:
119
+ logger.warning(f"Failed to parse date {date_header}: {e}")
120
+
121
+ # Extract content
122
+ content = self._extract_email_content(message)
123
+
124
+ return {
125
+ "date": email_date,
126
+ "time": email_time,
127
+ "subject": subject,
128
+ "from": from_header,
129
+ "content": content[:2000], # Limit content length
130
+ "message_id": message_id,
131
+ "gmail_id": message.get('id', '')
132
+ }
133
+
134
+ def search_emails(self, keyword: str, start_date: str, end_date: str) -> List[Dict]:
135
+ """Search emails containing keyword within date range using Gmail API
136
+
137
+ Args:
138
+ keyword: Keyword to search for in emails
139
+ start_date: Start date in DD-MMM-YYYY format
140
+ end_date: End date in DD-MMM-YYYY format
141
+
142
+ Returns:
143
+ List of email dictionaries
144
+ """
145
+ logger.info(f"Searching emails containing '{keyword}' between {start_date} and {end_date}")
146
+
147
+ # Get Gmail service
148
+ service = self.oauth_manager.get_gmail_service()
149
+ if not service:
150
+ raise Exception("Not authenticated. Please authenticate first using the setup tool.")
151
+
152
+ try:
153
+ # Parse dates
154
+ start_dt = self._parse_date_string(start_date)
155
+ end_dt = self._parse_date_string(end_date)
156
+
157
+ # Format dates for Gmail API query
158
+ after_date = self._format_date_for_query(start_dt)
159
+ before_date = self._format_date_for_query(end_dt + timedelta(days=1)) # Add 1 day for inclusive end
160
+
161
+ # Build search query
162
+ # Gmail API search syntax: https://developers.google.com/gmail/api/guides/filtering
163
+ query_parts = [
164
+ f'after:{after_date}',
165
+ f'before:{before_date}',
166
+ f'({keyword})' # Search in all fields
167
+ ]
168
+ query = ' '.join(query_parts)
169
+
170
+ logger.info(f"Gmail API query: {query}")
171
+
172
+ # Search for messages
173
+ results = service.users().messages().list(
174
+ userId='me',
175
+ q=query,
176
+ maxResults=500 # Limit to 500 results
177
+ ).execute()
178
+
179
+ messages = results.get('messages', [])
180
+ logger.info(f"Found {len(messages)} messages")
181
+
182
+ if not messages:
183
+ return []
184
+
185
+ # Fetch full message details
186
+ scraped_emails = []
187
+
188
+ for i, msg_ref in enumerate(messages):
189
+ try:
190
+ logger.info(f"Processing email {i+1}/{len(messages)}")
191
+
192
+ # Get full message
193
+ message = service.users().messages().get(
194
+ userId='me',
195
+ id=msg_ref['id'],
196
+ format='full'
197
+ ).execute()
198
+
199
+ # Parse message
200
+ parsed_email = self._parse_email_message(message)
201
+
202
+ # Verify date range (double-check since Gmail search might be inclusive)
203
+ email_dt = self._parse_date_string(parsed_email['date'])
204
+ if start_dt <= email_dt <= end_dt:
205
+ # Verify keyword presence (case-insensitive)
206
+ keyword_lower = keyword.lower()
207
+ if any(keyword_lower in text.lower() for text in [
208
+ parsed_email['subject'],
209
+ parsed_email['from'],
210
+ parsed_email['content']
211
+ ]):
212
+ scraped_emails.append(parsed_email)
213
+
214
+ except googleapiclient.errors.HttpError as e:
215
+ logger.error(f"Error fetching message {msg_ref['id']}: {e}")
216
+ continue
217
+ except Exception as e:
218
+ logger.error(f"Error processing message {msg_ref['id']}: {e}")
219
+ continue
220
+
221
+ # Sort by date (newest first)
222
+ scraped_emails.sort(
223
+ key=lambda x: datetime.strptime(f"{x['date']} {x['time']}", "%d-%b-%Y %H:%M:%S"),
224
+ reverse=True
225
+ )
226
+
227
+ logger.info(f"Successfully processed {len(scraped_emails)} emails containing '{keyword}'")
228
+ return scraped_emails
229
+
230
+ except googleapiclient.errors.HttpError as e:
231
+ logger.error(f"Gmail API error: {e}")
232
+ raise Exception(f"Gmail API error: {e}")
233
+ except Exception as e:
234
+ logger.error(f"Email search failed: {e}")
235
+ raise
236
+
237
+ def get_email_by_id(self, message_id: str) -> Optional[Dict]:
238
+ """Get email details by message ID or Gmail ID
239
+
240
+ Args:
241
+ message_id: Either the Message-ID header or Gmail message ID
242
+
243
+ Returns:
244
+ Email dictionary or None if not found
245
+ """
246
+ service = self.oauth_manager.get_gmail_service()
247
+ if not service:
248
+ raise Exception("Not authenticated. Please authenticate first using the setup tool.")
249
+
250
+ try:
251
+ # Try to get message directly by Gmail ID first
252
+ try:
253
+ message = service.users().messages().get(
254
+ userId='me',
255
+ id=message_id,
256
+ format='full'
257
+ ).execute()
258
+ return self._parse_email_message(message)
259
+ except googleapiclient.errors.HttpError:
260
+ # If direct ID lookup fails, search by Message-ID header
261
+ pass
262
+
263
+ # Search by Message-ID header
264
+ query = f'rfc822msgid:{message_id}'
265
+ results = service.users().messages().list(
266
+ userId='me',
267
+ q=query,
268
+ maxResults=1
269
+ ).execute()
270
+
271
+ messages = results.get('messages', [])
272
+ if not messages:
273
+ return None
274
+
275
+ # Get the message
276
+ message = service.users().messages().get(
277
+ userId='me',
278
+ id=messages[0]['id'],
279
+ format='full'
280
+ ).execute()
281
+
282
+ return self._parse_email_message(message)
283
+
284
+ except Exception as e:
285
+ logger.error(f"Failed to get email {message_id}: {e}")
286
+ return None
287
+
288
+ def is_authenticated(self) -> bool:
289
+ """Check if user is authenticated"""
290
+ return self.oauth_manager.is_authenticated()
291
+
292
+ def get_user_email(self) -> Optional[str]:
293
+ """Get authenticated user's email address"""
294
+ return self.oauth_manager.get_user_email()
295
+
296
+ def authenticate(self) -> bool:
297
+ """Trigger interactive authentication"""
298
+ return self.oauth_manager.authenticate_interactive()
299
+
300
+ # Global scraper instance
301
+ gmail_scraper = GmailAPIScraper()
agentic_implementation/oauth_manager.py ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import pickle
4
+ import base64
5
+ from pathlib import Path
6
+ from typing import Optional, Dict, Any
7
+ from cryptography.fernet import Fernet
8
+ import google.auth.transport.requests
9
+ import google_auth_oauthlib.flow
10
+ import googleapiclient.discovery
11
+ from google.oauth2.credentials import Credentials
12
+ from google.auth.transport.requests import Request
13
+ import webbrowser
14
+ import threading
15
+ import time
16
+ from http.server import HTTPServer, BaseHTTPRequestHandler
17
+ import urllib.parse as urlparse
18
+ from logger import logger
19
+
20
+ class OAuthCallbackHandler(BaseHTTPRequestHandler):
21
+ """HTTP request handler for OAuth callback"""
22
+
23
+ def do_GET(self):
24
+ """Handle GET request (OAuth callback)"""
25
+ # Parse the callback URL to extract authorization code
26
+ parsed_path = urlparse.urlparse(self.path)
27
+ query_params = urlparse.parse_qs(parsed_path.query)
28
+
29
+ if 'code' in query_params:
30
+ # Store the authorization code
31
+ self.server.auth_code = query_params['code'][0]
32
+
33
+ # Send success response
34
+ self.send_response(200)
35
+ self.send_header('Content-type', 'text/html')
36
+ self.end_headers()
37
+
38
+ success_html = """
39
+ <html>
40
+ <head><title>Authentication Successful</title></head>
41
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
42
+ <h1 style="color: #4CAF50;">βœ… Authentication Successful!</h1>
43
+ <p>You have successfully authenticated with Gmail.</p>
44
+ <p>You can now close this window and return to Claude Desktop.</p>
45
+ <script>
46
+ setTimeout(function() {
47
+ window.close();
48
+ }, 3000);
49
+ </script>
50
+ </body>
51
+ </html>
52
+ """
53
+ self.wfile.write(success_html.encode())
54
+ else:
55
+ # Send error response
56
+ self.send_response(400)
57
+ self.send_header('Content-type', 'text/html')
58
+ self.end_headers()
59
+
60
+ error_html = """
61
+ <html>
62
+ <head><title>Authentication Error</title></head>
63
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
64
+ <h1 style="color: #f44336;">❌ Authentication Failed</h1>
65
+ <p>There was an error during authentication.</p>
66
+ <p>Please try again.</p>
67
+ </body>
68
+ </html>
69
+ """
70
+ self.wfile.write(error_html.encode())
71
+
72
+ def log_message(self, format, *args):
73
+ """Suppress server log messages"""
74
+ pass
75
+
76
+ class GmailOAuthManager:
77
+ """Manages Gmail OAuth 2.0 authentication and token storage for multiple accounts"""
78
+
79
+ # Gmail API scopes
80
+ SCOPES = [
81
+ 'https://www.googleapis.com/auth/gmail.readonly',
82
+ 'https://www.googleapis.com/auth/gmail.modify'
83
+ ]
84
+
85
+ def __init__(self, credentials_dir: str = None):
86
+ """Initialize OAuth manager
87
+
88
+ Args:
89
+ credentials_dir: Directory to store credentials (defaults to ~/.mailquery_oauth)
90
+ """
91
+ if credentials_dir is None:
92
+ credentials_dir = os.path.expanduser("~/.mailquery_oauth")
93
+
94
+ self.credentials_dir = Path(credentials_dir)
95
+ self.credentials_dir.mkdir(exist_ok=True)
96
+
97
+ # File paths
98
+ self.client_secrets_file = self.credentials_dir / "client_secret.json"
99
+ self.accounts_file = self.credentials_dir / "accounts.json"
100
+ self.encryption_key_file = self.credentials_dir / "key.key"
101
+ self.current_account_file = self.credentials_dir / "current_account.txt"
102
+
103
+ # Initialize encryption
104
+ self._init_encryption()
105
+
106
+ # OAuth flow settings
107
+ self.redirect_uri = "http://localhost:8080/oauth2callback"
108
+
109
+ # Current account
110
+ self.current_account_email = self._load_current_account()
111
+
112
+ def _init_encryption(self):
113
+ """Initialize encryption for secure credential storage"""
114
+ if self.encryption_key_file.exists():
115
+ with open(self.encryption_key_file, 'rb') as key_file:
116
+ self.encryption_key = key_file.read()
117
+ else:
118
+ self.encryption_key = Fernet.generate_key()
119
+ with open(self.encryption_key_file, 'wb') as key_file:
120
+ key_file.write(self.encryption_key)
121
+ # Make key file readable only by owner
122
+ os.chmod(self.encryption_key_file, 0o600)
123
+
124
+ self.cipher_suite = Fernet(self.encryption_key)
125
+
126
+ def _load_current_account(self) -> Optional[str]:
127
+ """Load the currently selected account"""
128
+ if self.current_account_file.exists():
129
+ try:
130
+ with open(self.current_account_file, 'r') as f:
131
+ return f.read().strip()
132
+ except Exception as e:
133
+ logger.error(f"Failed to load current account: {e}")
134
+ return None
135
+
136
+ def _save_current_account(self, email: str):
137
+ """Save the currently selected account"""
138
+ try:
139
+ with open(self.current_account_file, 'w') as f:
140
+ f.write(email)
141
+ self.current_account_email = email
142
+ logger.info(f"Set current account to: {email}")
143
+ except Exception as e:
144
+ logger.error(f"Failed to save current account: {e}")
145
+
146
+ def setup_client_secrets(self, client_id: str, client_secret: str):
147
+ """Setup OAuth client secrets
148
+
149
+ Args:
150
+ client_id: Google OAuth 2.0 client ID
151
+ client_secret: Google OAuth 2.0 client secret
152
+ """
153
+ client_config = {
154
+ "web": {
155
+ "client_id": client_id,
156
+ "client_secret": client_secret,
157
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
158
+ "token_uri": "https://oauth2.googleapis.com/token",
159
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
160
+ "redirect_uris": [self.redirect_uri]
161
+ }
162
+ }
163
+
164
+ with open(self.client_secrets_file, 'w') as f:
165
+ json.dump(client_config, f, indent=2)
166
+
167
+ logger.info("Client secrets saved successfully")
168
+
169
+ def _encrypt_data(self, data: Any) -> bytes:
170
+ """Encrypt data using Fernet encryption"""
171
+ serialized_data = pickle.dumps(data)
172
+ return self.cipher_suite.encrypt(serialized_data)
173
+
174
+ def _decrypt_data(self, encrypted_data: bytes) -> Any:
175
+ """Decrypt data using Fernet encryption"""
176
+ decrypted_data = self.cipher_suite.decrypt(encrypted_data)
177
+ return pickle.loads(decrypted_data)
178
+
179
+ def get_authorization_url(self) -> str:
180
+ """Get the authorization URL for OAuth flow
181
+
182
+ Returns:
183
+ Authorization URL that user should visit
184
+ """
185
+ if not self.client_secrets_file.exists():
186
+ raise ValueError("Client secrets not found. Please run setup first.")
187
+
188
+ flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
189
+ str(self.client_secrets_file),
190
+ scopes=self.SCOPES
191
+ )
192
+ flow.redirect_uri = self.redirect_uri
193
+
194
+ auth_url, _ = flow.authorization_url(
195
+ access_type='offline',
196
+ include_granted_scopes='true',
197
+ prompt='consent' # Force consent to get refresh token
198
+ )
199
+
200
+ return auth_url
201
+
202
+ def authenticate_interactive(self) -> bool:
203
+ """Interactive authentication flow that opens browser
204
+
205
+ Returns:
206
+ True if authentication successful, False otherwise
207
+ """
208
+ try:
209
+ # Start local HTTP server for OAuth callback
210
+ server = HTTPServer(('localhost', 8080), OAuthCallbackHandler)
211
+ server.auth_code = None
212
+
213
+ # Get authorization URL
214
+ auth_url = self.get_authorization_url()
215
+
216
+ logger.info("Opening browser for authentication...")
217
+ logger.info(f"If browser doesn't open, visit: {auth_url}")
218
+
219
+ # Open browser
220
+ webbrowser.open(auth_url)
221
+
222
+ # Start server in background thread
223
+ server_thread = threading.Thread(target=server.handle_request)
224
+ server_thread.daemon = True
225
+ server_thread.start()
226
+
227
+ # Wait for callback (max 5 minutes)
228
+ timeout = 300 # 5 minutes
229
+ start_time = time.time()
230
+
231
+ while server.auth_code is None and (time.time() - start_time) < timeout:
232
+ time.sleep(1)
233
+
234
+ if server.auth_code is None:
235
+ logger.error("Authentication timed out")
236
+ return False
237
+
238
+ # Exchange authorization code for credentials
239
+ flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
240
+ str(self.client_secrets_file),
241
+ scopes=self.SCOPES
242
+ )
243
+ flow.redirect_uri = self.redirect_uri
244
+
245
+ flow.fetch_token(code=server.auth_code)
246
+ credentials = flow.credentials
247
+
248
+ # Get user email from credentials
249
+ user_email = self._get_email_from_credentials(credentials)
250
+ if not user_email:
251
+ logger.error("Failed to get user email from credentials")
252
+ return False
253
+
254
+ # Save encrypted credentials for this account
255
+ self._save_credentials(user_email, credentials)
256
+
257
+ # Set as current account
258
+ self._save_current_account(user_email)
259
+
260
+ logger.info("Authentication successful!")
261
+ return True
262
+
263
+ except Exception as e:
264
+ logger.error(f"Authentication failed: {e}")
265
+ return False
266
+
267
+ def _get_email_from_credentials(self, credentials: Credentials) -> Optional[str]:
268
+ """Get email address from credentials"""
269
+ try:
270
+ service = googleapiclient.discovery.build(
271
+ 'gmail', 'v1', credentials=credentials
272
+ )
273
+ profile = service.users().getProfile(userId='me').execute()
274
+ return profile.get('emailAddress')
275
+ except Exception as e:
276
+ logger.error(f"Failed to get email from credentials: {e}")
277
+ return None
278
+
279
+ def _save_credentials(self, email: str, credentials: Credentials):
280
+ """Save encrypted credentials for a specific account"""
281
+ try:
282
+ # Load existing accounts
283
+ accounts = self._load_accounts()
284
+
285
+ # Encrypt and store credentials
286
+ encrypted_credentials = self._encrypt_data(credentials)
287
+ accounts[email] = base64.b64encode(encrypted_credentials).decode('utf-8')
288
+
289
+ # Save accounts file
290
+ with open(self.accounts_file, 'w') as f:
291
+ json.dump(accounts, f, indent=2)
292
+
293
+ # Make accounts file readable only by owner
294
+ os.chmod(self.accounts_file, 0o600)
295
+
296
+ logger.info(f"Credentials saved for account: {email}")
297
+ except Exception as e:
298
+ logger.error(f"Failed to save credentials for {email}: {e}")
299
+ raise
300
+
301
+ def _load_accounts(self) -> Dict[str, str]:
302
+ """Load accounts data"""
303
+ if not self.accounts_file.exists():
304
+ return {}
305
+
306
+ try:
307
+ with open(self.accounts_file, 'r') as f:
308
+ return json.load(f)
309
+ except Exception as e:
310
+ logger.error(f"Failed to load accounts: {e}")
311
+ return {}
312
+
313
+ def _load_credentials(self, email: str) -> Optional[Credentials]:
314
+ """Load and decrypt credentials for a specific account"""
315
+ accounts = self._load_accounts()
316
+
317
+ if email not in accounts:
318
+ return None
319
+
320
+ try:
321
+ encrypted_credentials = base64.b64decode(accounts[email])
322
+ credentials = self._decrypt_data(encrypted_credentials)
323
+ return credentials
324
+ except Exception as e:
325
+ logger.error(f"Failed to load credentials for {email}: {e}")
326
+ return None
327
+
328
+ def get_valid_credentials(self, email: str = None) -> Optional[Credentials]:
329
+ """Get valid credentials for an account, refreshing if necessary
330
+
331
+ Args:
332
+ email: Email address of account (uses current account if None)
333
+
334
+ Returns:
335
+ Valid Credentials object or None if authentication required
336
+ """
337
+ if email is None:
338
+ email = self.current_account_email
339
+
340
+ if not email:
341
+ logger.warning("No current account set")
342
+ return None
343
+
344
+ credentials = self._load_credentials(email)
345
+
346
+ if not credentials:
347
+ logger.warning(f"No stored credentials found for {email}")
348
+ return None
349
+
350
+ # Refresh if expired
351
+ if credentials.expired and credentials.refresh_token:
352
+ try:
353
+ logger.info(f"Refreshing expired credentials for {email}...")
354
+ credentials.refresh(Request())
355
+ self._save_credentials(email, credentials)
356
+ logger.info("Credentials refreshed successfully")
357
+ except Exception as e:
358
+ logger.error(f"Failed to refresh credentials for {email}: {e}")
359
+ return None
360
+
361
+ if not credentials.valid:
362
+ logger.warning(f"Credentials are not valid for {email}")
363
+ return None
364
+
365
+ return credentials
366
+
367
+ def is_authenticated(self, email: str = None) -> bool:
368
+ """Check if user is authenticated
369
+
370
+ Args:
371
+ email: Email address to check (uses current account if None)
372
+
373
+ Returns:
374
+ True if valid credentials exist, False otherwise
375
+ """
376
+ return self.get_valid_credentials(email) is not None
377
+
378
+ def switch_account(self, email: str) -> bool:
379
+ """Switch to a different authenticated account
380
+
381
+ Args:
382
+ email: Email address to switch to
383
+
384
+ Returns:
385
+ True if switch successful, False if account not found or not authenticated
386
+ """
387
+ if self.is_authenticated(email):
388
+ self._save_current_account(email)
389
+ logger.info(f"Switched to account: {email}")
390
+ return True
391
+ else:
392
+ logger.error(f"Account {email} is not authenticated")
393
+ return False
394
+
395
+ def list_accounts(self) -> Dict[str, bool]:
396
+ """List all stored accounts and their authentication status
397
+
398
+ Returns:
399
+ Dictionary mapping email addresses to authentication status
400
+ """
401
+ accounts = self._load_accounts()
402
+ result = {}
403
+
404
+ for email in accounts.keys():
405
+ result[email] = self.is_authenticated(email)
406
+
407
+ return result
408
+
409
+ def remove_account(self, email: str):
410
+ """Remove an account and its credentials
411
+
412
+ Args:
413
+ email: Email address to remove
414
+ """
415
+ accounts = self._load_accounts()
416
+
417
+ if email in accounts:
418
+ del accounts[email]
419
+
420
+ # Save updated accounts
421
+ with open(self.accounts_file, 'w') as f:
422
+ json.dump(accounts, f, indent=2)
423
+
424
+ # If this was the current account, clear it
425
+ if self.current_account_email == email:
426
+ if self.current_account_file.exists():
427
+ self.current_account_file.unlink()
428
+ self.current_account_email = None
429
+
430
+ logger.info(f"Removed account: {email}")
431
+ else:
432
+ logger.warning(f"Account {email} not found")
433
+
434
+ def clear_credentials(self):
435
+ """Clear all stored credentials"""
436
+ if self.accounts_file.exists():
437
+ self.accounts_file.unlink()
438
+ if self.current_account_file.exists():
439
+ self.current_account_file.unlink()
440
+ self.current_account_email = None
441
+ logger.info("All credentials cleared")
442
+
443
+ def get_gmail_service(self, email: str = None):
444
+ """Get authenticated Gmail service object
445
+
446
+ Args:
447
+ email: Email address (uses current account if None)
448
+
449
+ Returns:
450
+ Gmail service object or None if not authenticated
451
+ """
452
+ credentials = self.get_valid_credentials(email)
453
+ if not credentials:
454
+ return None
455
+
456
+ try:
457
+ service = googleapiclient.discovery.build(
458
+ 'gmail', 'v1', credentials=credentials
459
+ )
460
+ return service
461
+ except Exception as e:
462
+ logger.error(f"Failed to build Gmail service: {e}")
463
+ return None
464
+
465
+ def get_user_email(self, email: str = None) -> Optional[str]:
466
+ """Get the authenticated user's email address
467
+
468
+ Args:
469
+ email: Email address (uses current account if None)
470
+
471
+ Returns:
472
+ User's email address or None if not authenticated
473
+ """
474
+ if email is None:
475
+ return self.current_account_email
476
+ return email if self.is_authenticated(email) else None
477
+
478
+ def get_current_account(self) -> Optional[str]:
479
+ """Get the currently selected account
480
+
481
+ Returns:
482
+ Current account email or None if no account selected
483
+ """
484
+ return self.current_account_email
485
+
486
+ # Global OAuth manager instance
487
+ oauth_manager = GmailOAuthManager()
agentic_implementation/requirements_oauth.txt ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies from original requirements
2
+ gradio>=4.0.0
3
+ python-dotenv
4
+ beautifulsoup4
5
+ requests
6
+
7
+ # OAuth and Google API dependencies
8
+ google-auth>=2.15.0
9
+ google-auth-oauthlib>=1.0.0
10
+ google-auth-httplib2>=0.2.0
11
+ google-api-python-client>=2.70.0
12
+
13
+ # Encryption for secure credential storage
14
+ cryptography>=3.4.8
15
+
16
+ # Existing dependencies (if not already included)
17
+ openai
18
+ pandas
19
+ numpy
20
+ matplotlib
21
+ seaborn
agentic_implementation/setup_oauth.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ OAuth Setup Utility for Gmail MCP Server
4
+
5
+ This script helps users set up OAuth authentication for the Gmail MCP server.
6
+ """
7
+
8
+ import sys
9
+ import os
10
+ import json
11
+ from pathlib import Path
12
+ from oauth_manager import oauth_manager
13
+ from logger import logger
14
+
15
+ def print_banner():
16
+ """Print setup banner"""
17
+ print("=" * 60)
18
+ print("πŸ“§ Gmail MCP Server - OAuth Setup")
19
+ print("=" * 60)
20
+ print()
21
+
22
+ def print_step(step_num: int, title: str):
23
+ """Print step header"""
24
+ print(f"\nπŸ”Ή Step {step_num}: {title}")
25
+ print("-" * 50)
26
+
27
+ def check_dependencies():
28
+ """Check if required dependencies are installed"""
29
+ try:
30
+ import google.auth
31
+ import google_auth_oauthlib
32
+ import googleapiclient
33
+ import cryptography
34
+ print("βœ… All required dependencies are installed")
35
+ return True
36
+ except ImportError as e:
37
+ print(f"❌ Missing dependency: {e}")
38
+ print("\nPlease install the required dependencies:")
39
+ print("pip install google-auth google-auth-oauthlib google-api-python-client cryptography")
40
+ return False
41
+
42
+ def setup_google_cloud_project():
43
+ """Guide user through Google Cloud project setup"""
44
+ print_step(1, "Google Cloud Project Setup")
45
+
46
+ print("You need to create a Google Cloud project and enable the Gmail API.")
47
+ print("\nπŸ“‹ Follow these steps:")
48
+ print("1. Go to: https://console.cloud.google.com/")
49
+ print("2. Create a new project or select an existing one")
50
+ print("3. Enable the Gmail API:")
51
+ print(" - Go to 'APIs & Services' > 'Library'")
52
+ print(" - Search for 'Gmail API'")
53
+ print(" - Click 'Enable'")
54
+
55
+ input("\nβœ… Press Enter when you've completed these steps...")
56
+
57
+ def setup_oauth_consent():
58
+ """Guide user through OAuth consent screen setup"""
59
+ print_step(2, "OAuth Consent Screen Setup")
60
+
61
+ print("Now you need to configure the OAuth consent screen.")
62
+ print("\nπŸ“‹ Follow these steps:")
63
+ print("1. Go to: https://console.cloud.google.com/apis/credentials/consent")
64
+ print("2. Choose 'External' user type (unless using Google Workspace)")
65
+ print("3. Fill in the app information:")
66
+ print(" - App name: 'Gmail MCP Server' (or your preferred name)")
67
+ print(" - User support email: Your email address")
68
+ print(" - Developer contact: Your email address")
69
+ print("4. Add these scopes:")
70
+ print(" - https://www.googleapis.com/auth/gmail.readonly")
71
+ print(" - https://www.googleapis.com/auth/gmail.modify")
72
+ print("5. Add your email as a test user")
73
+ print("6. Complete the setup")
74
+
75
+ input("\nβœ… Press Enter when you've completed these steps...")
76
+
77
+ def setup_oauth_credentials():
78
+ """Guide user through OAuth credentials setup"""
79
+ print_step(3, "OAuth Client Credentials Setup")
80
+
81
+ print("Now you need to create OAuth 2.0 client credentials.")
82
+ print("\nπŸ“‹ Follow these steps:")
83
+ print("1. Go to: https://console.cloud.google.com/apis/credentials")
84
+ print("2. Click 'Create Credentials' > 'OAuth client ID'")
85
+ print("3. Choose 'Web application' as the application type")
86
+ print("4. Set the name to 'Gmail MCP Server'")
87
+ print("5. Add this redirect URI:")
88
+ print(" http://localhost:8080/oauth2callback")
89
+ print("6. Click 'Create'")
90
+ print("7. Copy the Client ID and Client Secret")
91
+
92
+ print("\nπŸ”‘ Enter your OAuth credentials:")
93
+
94
+ while True:
95
+ client_id = input("Client ID: ").strip()
96
+ if client_id:
97
+ break
98
+ print("❌ Client ID cannot be empty")
99
+
100
+ while True:
101
+ client_secret = input("Client Secret: ").strip()
102
+ if client_secret:
103
+ break
104
+ print("❌ Client Secret cannot be empty")
105
+
106
+ try:
107
+ oauth_manager.setup_client_secrets(client_id, client_secret)
108
+ print("βœ… OAuth credentials saved successfully")
109
+ return True
110
+ except Exception as e:
111
+ print(f"❌ Failed to save credentials: {e}")
112
+ return False
113
+
114
+ def test_authentication():
115
+ """Test the OAuth authentication flow"""
116
+ print_step(4, "Authentication Test")
117
+
118
+ print("Now let's test the authentication flow.")
119
+ print("This will open your web browser for authentication.")
120
+
121
+ confirm = input("\n🌐 Ready to open browser for authentication? (y/n): ").strip().lower()
122
+ if confirm != 'y':
123
+ print("Authentication test skipped.")
124
+ return False
125
+
126
+ try:
127
+ print("\nπŸ”„ Starting authentication flow...")
128
+ success = oauth_manager.authenticate_interactive()
129
+
130
+ if success:
131
+ print("βœ… Authentication successful!")
132
+
133
+ # Test getting user info
134
+ user_email = oauth_manager.get_user_email()
135
+ if user_email:
136
+ print(f"βœ… Authenticated as: {user_email}")
137
+
138
+ return True
139
+ else:
140
+ print("❌ Authentication failed")
141
+ return False
142
+
143
+ except Exception as e:
144
+ print(f"❌ Authentication error: {e}")
145
+ return False
146
+
147
+ def show_completion_info():
148
+ """Show completion information and next steps"""
149
+ print("\n" + "=" * 60)
150
+ print("πŸŽ‰ Setup Complete!")
151
+ print("=" * 60)
152
+
153
+ print("\nβœ… Your Gmail MCP server is now configured with OAuth authentication!")
154
+ print("\nπŸ“ Next steps:")
155
+ print("1. Start the MCP server:")
156
+ print(" python email_mcp_server_oauth.py")
157
+ print("\n2. Configure Claude Desktop:")
158
+ print(' Add this to your MCP configuration:')
159
+ print(' {')
160
+ print(' "mcpServers": {')
161
+ print(' "gmail-oauth": {')
162
+ print(' "command": "npx",')
163
+ print(' "args": ["mcp-remote", "http://localhost:7860/gradio_api/mcp/sse"]')
164
+ print(' }')
165
+ print(' }')
166
+ print(' }')
167
+
168
+ print("\nπŸ” Security notes:")
169
+ print("- Your credentials are encrypted and stored locally")
170
+ print("- Tokens are automatically refreshed when needed")
171
+ print("- You can revoke access anytime from Google Account settings")
172
+
173
+ credentials_dir = oauth_manager.credentials_dir
174
+ print(f"\nπŸ“ Credentials stored in: {credentials_dir}")
175
+
176
+ def show_help():
177
+ """Show help information"""
178
+ print("Gmail MCP Server OAuth Setup")
179
+ print("\nUsage:")
180
+ print(" python setup_oauth.py # Full interactive setup")
181
+ print(" python setup_oauth.py --help # Show this help")
182
+ print(" python setup_oauth.py --auth # Re-authenticate only")
183
+ print(" python setup_oauth.py --status # Check authentication status")
184
+ print(" python setup_oauth.py --clear # Clear stored credentials")
185
+
186
+ def check_status():
187
+ """Check authentication status"""
188
+ print("πŸ” Checking authentication status...")
189
+
190
+ if oauth_manager.is_authenticated():
191
+ user_email = oauth_manager.get_user_email()
192
+ print(f"βœ… Authenticated as: {user_email}")
193
+ return True
194
+ else:
195
+ print("❌ Not authenticated")
196
+ return False
197
+
198
+ def clear_credentials():
199
+ """Clear stored credentials"""
200
+ confirm = input("⚠️ This will clear all stored credentials. Continue? (y/n): ").strip().lower()
201
+ if confirm == 'y':
202
+ oauth_manager.clear_credentials()
203
+ print("βœ… Credentials cleared")
204
+ else:
205
+ print("Operation cancelled")
206
+
207
+ def main():
208
+ """Main setup function"""
209
+ if len(sys.argv) > 1:
210
+ arg = sys.argv[1].lower()
211
+
212
+ if arg in ['--help', '-h', 'help']:
213
+ show_help()
214
+ return
215
+ elif arg == '--status':
216
+ check_status()
217
+ return
218
+ elif arg == '--auth':
219
+ print("πŸ”„ Starting re-authentication...")
220
+ if test_authentication():
221
+ print("βœ… Re-authentication successful")
222
+ else:
223
+ print("❌ Re-authentication failed")
224
+ return
225
+ elif arg == '--clear':
226
+ clear_credentials()
227
+ return
228
+ else:
229
+ print(f"Unknown argument: {arg}")
230
+ show_help()
231
+ return
232
+
233
+ # Full interactive setup
234
+ print_banner()
235
+
236
+ # Check if already authenticated
237
+ if oauth_manager.is_authenticated():
238
+ user_email = oauth_manager.get_user_email()
239
+ print(f"βœ… Already authenticated as: {user_email}")
240
+
241
+ choice = input("\nπŸ”„ Do you want to re-authenticate? (y/n): ").strip().lower()
242
+ if choice == 'y':
243
+ if test_authentication():
244
+ show_completion_info()
245
+ else:
246
+ print("Setup complete - you're already authenticated!")
247
+ return
248
+
249
+ # Check dependencies
250
+ if not check_dependencies():
251
+ return
252
+
253
+ # Full setup flow
254
+ try:
255
+ setup_google_cloud_project()
256
+ setup_oauth_consent()
257
+
258
+ if not setup_oauth_credentials():
259
+ print("❌ Setup failed at credentials step")
260
+ return
261
+
262
+ if test_authentication():
263
+ show_completion_info()
264
+ else:
265
+ print("❌ Setup completed but authentication test failed")
266
+ print("You can try authentication later with: python setup_oauth.py --auth")
267
+
268
+ except KeyboardInterrupt:
269
+ print("\n\n⚠️ Setup interrupted by user")
270
+ except Exception as e:
271
+ print(f"\n❌ Setup failed: {e}")
272
+
273
+ if __name__ == "__main__":
274
+ main()