Da-123 commited on
Commit
2312d97
Β·
verified Β·
1 Parent(s): 5c85daa

MCP connection (#9)

Browse files

- mcp server refactor done (08d701a79043690507e7125c35ceb5c1e9fa4063)

agentic_implementation/README_MCP.md ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Email Assistant MCP Server
2
+
3
+ This is a Gradio-based MCP (Model Context Protocol) server that allows Claude Desktop to interact with your Gmail emails.
4
+
5
+ ## Features
6
+
7
+ - **Email Search**: Search your emails using natural language queries
8
+ - **Email Details**: Get full details of specific emails by message ID
9
+ - **Pattern Analysis**: Analyze email patterns from specific senders over time
10
+
11
+ ## Setup
12
+
13
+ 1. **Install Dependencies**:
14
+ ```bash
15
+ pip install -r requirements_mcp.txt
16
+ ```
17
+
18
+ 2. **Set up Gmail App Password**:
19
+ - Enable 2-Factor Authentication on your Gmail account
20
+ - Generate an App Password: https://support.google.com/accounts/answer/185833
21
+ - Keep your Gmail address and app password ready
22
+
23
+ 3. **Run the MCP Server**:
24
+ ```bash
25
+ python email_mcp_server.py
26
+ ```
27
+
28
+ The server will start and show you the MCP endpoint URL, typically:
29
+ ```
30
+ http://localhost:7860/gradio_api/mcp/sse
31
+ ```
32
+
33
+ ## Claude Desktop Configuration
34
+
35
+ Add this configuration to your Claude Desktop MCP settings:
36
+
37
+ **For SSE-supported clients:**
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "email-assistant": {
42
+ "url": "http://localhost:7860/gradio_api/mcp/sse"
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ **For Claude Desktop (requires mcp-remote):**
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "email-assistant": {
53
+ "command": "npx",
54
+ "args": [
55
+ "mcp-remote",
56
+ "http://localhost:7860/gradio_api/mcp/sse"
57
+ ]
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Available Tools
64
+
65
+ ### 1. search_emails
66
+ Search your emails using natural language queries.
67
+
68
+ **Parameters:**
69
+ - `email_address`: Your Gmail address
70
+ - `app_password`: Your Gmail app password
71
+ - `query`: Natural language query (e.g., "show me emails from amazon last week")
72
+
73
+ **Example Usage in Claude:**
74
+ > "Can you search my emails for messages from Swiggy in the last week? My email is [email protected] and my app password is xxxx-xxxx-xxxx-xxxx"
75
+
76
+ ### 2. get_email_details
77
+ Get full details of a specific email by message ID.
78
+
79
+ **Parameters:**
80
+ - `email_address`: Your Gmail address
81
+ - `app_password`: Your Gmail app password
82
+ - `message_id`: Message ID from search results
83
+
84
+ ### 3. analyze_email_patterns
85
+ Analyze email patterns from a specific sender over time.
86
+
87
+ **Parameters:**
88
+ - `email_address`: Your Gmail address
89
+ - `app_password`: Your Gmail app password
90
+ - `sender_keyword`: Sender to analyze (e.g., "amazon", "google")
91
+ - `days_back`: Number of days to analyze (default: "30")
92
+
93
+ ## Security Notes
94
+
95
+ - Your email credentials are only used for the duration of each tool call
96
+ - Credentials are not stored or logged by the server
97
+ - All communication happens locally on your machine
98
+ - The server only exposes the MCP interface, not a public web interface
99
+
100
+ ## Troubleshooting
101
+
102
+ 1. **Connection Issues**: Make sure your Gmail app password is correct and 2FA is enabled
103
+ 2. **MCP Client Issues**: Try restarting Claude Desktop after configuration changes
104
+ 3. **Search Issues**: The tool searches in FROM, SUBJECT, and BODY fields for keywords
105
+
106
+ ## Example Queries
107
+
108
+ Once configured with Claude Desktop, you can ask:
109
+
110
+ - "Search my emails for messages from Amazon in the last month"
111
+ - "Show me emails from my bank from last week"
112
+ - "Analyze my LinkedIn email patterns over the last 60 days"
113
+ - "Find emails from Swiggy today"
114
+
115
+ Claude will automatically call the appropriate tools with your provided credentials.
agentic_implementation/email_mcp_server.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 your existing modules
9
+ from tools import extract_query_info, analyze_emails
10
+ from email_scraper import scrape_emails_by_text_search_with_credentials, _load_email_db
11
+ from logger import logger
12
+
13
+ load_dotenv()
14
+
15
+ def search_emails(email_address: str, app_password: str, query: str) -> str:
16
+ """
17
+ Search for emails based on a natural language query and return a summary.
18
+
19
+ Args:
20
+ email_address (str): The Gmail address to connect to
21
+ app_password (str): The Gmail app password for authentication
22
+ query (str): Natural language query (e.g., "show me mails from swiggy last week")
23
+
24
+ Returns:
25
+ str: JSON string containing email search results and analysis
26
+ """
27
+ try:
28
+ logger.info("Email MCP tool called with query: %s", query)
29
+
30
+ # Extract sender keyword and date range from query
31
+ query_info = extract_query_info(query)
32
+ sender_keyword = query_info.get("sender_keyword", "")
33
+ start_date = query_info.get("start_date")
34
+ end_date = query_info.get("end_date")
35
+
36
+ print(f"Searching for emails with keyword '{sender_keyword}' between {start_date} and {end_date}")
37
+
38
+ # Use the modified scraper function that accepts credentials
39
+ full_emails = scrape_emails_by_text_search_with_credentials(
40
+ email_address, app_password, sender_keyword, start_date, end_date
41
+ )
42
+
43
+ if not full_emails:
44
+ result = {
45
+ "query_info": query_info,
46
+ "email_summary": [],
47
+ "analysis": {"summary": f"No emails found for '{sender_keyword}' in the specified date range.", "insights": []},
48
+ "email_count": 0
49
+ }
50
+ return json.dumps(result, indent=2)
51
+
52
+ # Create summary version without full content
53
+ email_summary = []
54
+ for email in full_emails:
55
+ summary_email = {
56
+ "date": email.get("date"),
57
+ "time": email.get("time"),
58
+ "subject": email.get("subject"),
59
+ "from": email.get("from", "Unknown Sender"),
60
+ "message_id": email.get("message_id")
61
+ }
62
+ email_summary.append(summary_email)
63
+
64
+ # Auto-analyze the emails for insights
65
+ analysis = analyze_emails(full_emails)
66
+
67
+ # Return summary info with analysis
68
+ result = {
69
+ "query_info": query_info,
70
+ "email_summary": email_summary,
71
+ "analysis": analysis,
72
+ "email_count": len(full_emails)
73
+ }
74
+
75
+ return json.dumps(result, indent=2)
76
+
77
+ except Exception as e:
78
+ logger.error("Error in search_emails: %s", e)
79
+ error_result = {
80
+ "error": str(e),
81
+ "query": query,
82
+ "message": "Failed to search emails. Please check your credentials and try again."
83
+ }
84
+ return json.dumps(error_result, indent=2)
85
+
86
+
87
+ def get_email_details(email_address: str, app_password: str, message_id: str) -> str:
88
+ """
89
+ Get full details of a specific email by its message ID.
90
+
91
+ Args:
92
+ email_address (str): The Gmail address to connect to
93
+ app_password (str): The Gmail app password for authentication
94
+ message_id (str): The message ID of the email to retrieve
95
+
96
+ Returns:
97
+ str: JSON string containing the full email details
98
+ """
99
+ try:
100
+ logger.info("Getting email details for message_id: %s", message_id)
101
+
102
+ # Load from local cache first
103
+ db = _load_email_db()
104
+
105
+ # Search each sender's email list
106
+ for sender_data in db.values():
107
+ for email in sender_data.get("emails", []):
108
+ if email.get("message_id") == message_id:
109
+ return json.dumps(email, indent=2)
110
+
111
+ # If not found in cache
112
+ error_result = {
113
+ "error": f"No email found with message_id '{message_id}'",
114
+ "message": "Email may not be in local cache. Try searching for emails first."
115
+ }
116
+ return json.dumps(error_result, indent=2)
117
+
118
+ except Exception as e:
119
+ logger.error("Error in get_email_details: %s", e)
120
+ error_result = {
121
+ "error": str(e),
122
+ "message_id": message_id,
123
+ "message": "Failed to retrieve email details."
124
+ }
125
+ return json.dumps(error_result, indent=2)
126
+
127
+
128
+ def analyze_email_patterns(email_address: str, app_password: str, sender_keyword: str, days_back: str = "30") -> str:
129
+ """
130
+ Analyze email patterns from a specific sender over a given time period.
131
+
132
+ Args:
133
+ email_address (str): The Gmail address to connect to
134
+ app_password (str): The Gmail app password for authentication
135
+ sender_keyword (str): The sender/company keyword to analyze (e.g., "amazon", "google")
136
+ days_back (str): Number of days to look back (default: "30")
137
+
138
+ Returns:
139
+ str: JSON string containing email pattern analysis
140
+ """
141
+ try:
142
+ logger.info("Analyzing email patterns for sender: %s, days_back: %s", sender_keyword, days_back)
143
+
144
+ # Calculate date range
145
+ days_int = int(days_back)
146
+ end_date = datetime.today()
147
+ start_date = end_date - timedelta(days=days_int)
148
+
149
+ start_date_str = start_date.strftime("%d-%b-%Y")
150
+ end_date_str = end_date.strftime("%d-%b-%Y")
151
+
152
+ # Search for emails
153
+ full_emails = scrape_emails_by_text_search_with_credentials(
154
+ email_address, app_password, sender_keyword, start_date_str, end_date_str
155
+ )
156
+
157
+ if not full_emails:
158
+ result = {
159
+ "sender_keyword": sender_keyword,
160
+ "date_range": f"{start_date_str} to {end_date_str}",
161
+ "analysis": {"summary": f"No emails found from '{sender_keyword}' in the last {days_back} days.", "insights": []},
162
+ "email_count": 0
163
+ }
164
+ return json.dumps(result, indent=2)
165
+
166
+ # Analyze the emails
167
+ analysis = analyze_emails(full_emails)
168
+
169
+ result = {
170
+ "sender_keyword": sender_keyword,
171
+ "date_range": f"{start_date_str} to {end_date_str}",
172
+ "analysis": analysis,
173
+ "email_count": len(full_emails)
174
+ }
175
+
176
+ return json.dumps(result, indent=2)
177
+
178
+ except Exception as e:
179
+ logger.error("Error in analyze_email_patterns: %s", e)
180
+ error_result = {
181
+ "error": str(e),
182
+ "sender_keyword": sender_keyword,
183
+ "message": "Failed to analyze email patterns."
184
+ }
185
+ return json.dumps(error_result, indent=2)
186
+
187
+
188
+ # Create the Gradio interface for email search
189
+ search_interface = gr.Interface(
190
+ fn=search_emails,
191
+ inputs=[
192
+ gr.Textbox(label="Email Address", placeholder="[email protected]"),
193
+ gr.Textbox(label="App Password", type="password", placeholder="Your Gmail app password"),
194
+ gr.Textbox(label="Query", placeholder="Show me emails from amazon last week")
195
+ ],
196
+ outputs=gr.Textbox(label="Search Results", lines=20),
197
+ title="Email Search",
198
+ description="Search your emails using natural language queries"
199
+ )
200
+
201
+ # Create the Gradio interface for email details
202
+ details_interface = gr.Interface(
203
+ fn=get_email_details,
204
+ inputs=[
205
+ gr.Textbox(label="Email Address", placeholder="[email protected]"),
206
+ gr.Textbox(label="App Password", type="password", placeholder="Your Gmail app password"),
207
+ gr.Textbox(label="Message ID", placeholder="Email message ID from search results")
208
+ ],
209
+ outputs=gr.Textbox(label="Email Details", lines=20),
210
+ title="Email Details",
211
+ description="Get full details of a specific email by message ID"
212
+ )
213
+
214
+ # Create the Gradio interface for email pattern analysis
215
+ analysis_interface = gr.Interface(
216
+ fn=analyze_email_patterns,
217
+ inputs=[
218
+ gr.Textbox(label="Email Address", placeholder="[email protected]"),
219
+ gr.Textbox(label="App Password", type="password", placeholder="Your Gmail app password"),
220
+ gr.Textbox(label="Sender Keyword", placeholder="amazon, google, linkedin, etc."),
221
+ gr.Textbox(label="Days Back", value="30", placeholder="Number of days to analyze")
222
+ ],
223
+ outputs=gr.Textbox(label="Analysis Results", lines=20),
224
+ title="Email Pattern Analysis",
225
+ description="Analyze email patterns from a specific sender over time"
226
+ )
227
+
228
+ # Combine interfaces into a tabbed interface
229
+ demo = gr.TabbedInterface(
230
+ [search_interface, details_interface, analysis_interface],
231
+ ["Email Search", "Email Details", "Pattern Analysis"],
232
+ title="πŸ“§ Email Assistant MCP Server"
233
+ )
234
+
235
+ if __name__ == "__main__":
236
+ # Set environment variable to enable MCP server
237
+ import os
238
+ os.environ["GRADIO_MCP_SERVER"] = "True"
239
+
240
+ # Launch the server
241
+ demo.launch(share=False)
242
+
243
+ print("\nπŸš€ MCP Server is running!")
244
+ print("πŸ“ MCP Endpoint: http://localhost:7860/gradio_api/mcp/sse")
245
+ print("πŸ“– Copy this URL to your Claude Desktop MCP configuration")
agentic_implementation/email_scraper.py CHANGED
@@ -287,6 +287,144 @@ def scrape_emails_by_text_search(keyword: str, start_date: str, end_date: str) -
287
  print(f"Email text search failed: {e}")
288
  raise
289
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  # Test the scraper
291
  if __name__ == "__main__":
292
  # Test scraping
 
287
  print(f"Email text search failed: {e}")
288
  raise
289
 
290
+ def scrape_emails_by_text_search_with_credentials(email_id: str, app_password: str, keyword: str, start_date: str, end_date: str) -> List[Dict]:
291
+ """
292
+ Scrape emails containing a specific keyword (like company name) within date range.
293
+ Uses provided credentials instead of environment variables.
294
+
295
+ Args:
296
+ email_id: Gmail address
297
+ app_password: Gmail app password
298
+ keyword: Keyword to search for
299
+ start_date: Start date in DD-MMM-YYYY format
300
+ end_date: End date in DD-MMM-YYYY format
301
+ """
302
+ print(f"Searching emails containing '{keyword}' between {start_date} and {end_date}")
303
+
304
+ if not email_id or not app_password:
305
+ raise Exception("Email ID and App Password are required")
306
+
307
+ try:
308
+ # Connect using provided credentials
309
+ print("=== IMAP Connection Debug ===")
310
+ print(f"Email ID: {email_id[:5]}...@{email_id.split('@')[1] if '@' in email_id else 'INVALID'}")
311
+ print("App password: [PROVIDED]")
312
+
313
+ print("πŸ”„ Attempting IMAP SSL connection to imap.gmail.com:993...")
314
+ mail = imaplib.IMAP4_SSL("imap.gmail.com")
315
+ print("βœ… SSL connection established")
316
+
317
+ print("πŸ”„ Attempting login...")
318
+ result = mail.login(email_id, app_password)
319
+ print(f"βœ… Login successful: {result}")
320
+
321
+ print("πŸ”„ Selecting mailbox: [Gmail]/All Mail...")
322
+ result = mail.select('"[Gmail]/All Mail"')
323
+ print(f"βœ… Mailbox selected: {result}")
324
+
325
+ # Prepare IMAP search criteria with text search
326
+ start_imap = _date_to_imap_format(start_date)
327
+ # Add one day to end_date for BEFORE criteria (IMAP BEFORE is exclusive)
328
+ end_dt = datetime.strptime(end_date, "%d-%b-%Y") + timedelta(days=1)
329
+ end_imap = end_dt.strftime("%d-%b-%Y")
330
+
331
+ # Search for emails containing the keyword in FROM field or SUBJECT or BODY
332
+ # We'll search multiple criteria and combine results
333
+ search_criteria_list = [
334
+ f'FROM "{keyword}" SINCE "{start_imap}" BEFORE "{end_imap}"',
335
+ f'SUBJECT "{keyword}" SINCE "{start_imap}" BEFORE "{end_imap}"',
336
+ f'BODY "{keyword}" SINCE "{start_imap}" BEFORE "{end_imap}"'
337
+ ]
338
+
339
+ all_email_ids = set()
340
+
341
+ # Search with multiple criteria to catch emails containing the keyword
342
+ for search_criteria in search_criteria_list:
343
+ try:
344
+ print(f"IMAP search: {search_criteria}")
345
+ status, data = mail.search(None, search_criteria)
346
+ if status == 'OK' and data[0]:
347
+ email_ids = data[0].split()
348
+ all_email_ids.update(email_ids)
349
+ print(f"Found {len(email_ids)} emails with this criteria")
350
+ except Exception as e:
351
+ print(f"Search criteria failed: {search_criteria}, error: {e}")
352
+ continue
353
+
354
+ print(f"Total unique emails found: {len(all_email_ids)}")
355
+ scraped_emails = []
356
+
357
+ # Process each email
358
+ for i, email_id in enumerate(all_email_ids):
359
+ try:
360
+ print(f"Processing email {i+1}/{len(all_email_ids)}")
361
+
362
+ # Fetch email
363
+ status, msg_data = mail.fetch(email_id, "(RFC822)")
364
+ if status != 'OK':
365
+ continue
366
+
367
+ # Parse email
368
+ msg = message_from_bytes(msg_data[0][1])
369
+
370
+ # Extract information
371
+ subject = msg.get("Subject", "No Subject")
372
+ from_header = msg.get("From", "Unknown Sender")
373
+ content = _email_to_clean_text(msg)
374
+
375
+ # Check if the keyword is actually present (case-insensitive)
376
+ keyword_lower = keyword.lower()
377
+ if not any(keyword_lower in text.lower() for text in [subject, from_header, content]):
378
+ continue
379
+
380
+ # Parse date
381
+ date_header = msg.get("Date", "")
382
+ if date_header:
383
+ try:
384
+ dt_obj = parsedate_to_datetime(date_header)
385
+ # Convert to IST
386
+ ist_dt = dt_obj.astimezone(ZoneInfo("Asia/Kolkata"))
387
+ email_date = ist_dt.strftime("%d-%b-%Y")
388
+ email_time = ist_dt.strftime("%H:%M:%S")
389
+ except:
390
+ email_date = datetime.today().strftime("%d-%b-%Y")
391
+ email_time = "00:00:00"
392
+ else:
393
+ email_date = datetime.today().strftime("%d-%b-%Y")
394
+ email_time = "00:00:00"
395
+
396
+ # Double-check date range
397
+ if not _is_date_in_range(email_date, start_date, end_date):
398
+ continue
399
+
400
+ # Get message ID for deduplication
401
+ message_id = msg.get("Message-ID", f"missing-{email_id.decode()}")
402
+
403
+ scraped_emails.append({
404
+ "date": email_date,
405
+ "time": email_time,
406
+ "subject": subject,
407
+ "from": from_header,
408
+ "content": content[:2000], # Limit content length
409
+ "message_id": message_id
410
+ })
411
+
412
+ except Exception as e:
413
+ print(f"Error processing email {email_id}: {e}")
414
+ continue
415
+
416
+ mail.logout()
417
+
418
+ # Sort by date (newest first)
419
+ scraped_emails.sort(key=lambda x: datetime.strptime(f"{x['date']} {x['time']}", "%d-%b-%Y %H:%M:%S"), reverse=True)
420
+
421
+ print(f"Successfully processed {len(scraped_emails)} emails containing '{keyword}'")
422
+ return scraped_emails
423
+
424
+ except Exception as e:
425
+ print(f"Email text search failed: {e}")
426
+ raise
427
+
428
  # Test the scraper
429
  if __name__ == "__main__":
430
  # Test scraping