mseep-email-client 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Zilong Xue
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: mseep-email_client
3
+ Version: 0.1.1
4
+ Summary: Email search and management tool using MCP protocol
5
+ Author-email: mseep <support@skydeck.ai>
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/plain
8
+ License-File: LICENSE
9
+ Requires-Dist: httpx>=0.28.1
10
+ Requires-Dist: mcp>=1.1.2
11
+ Requires-Dist: python-dotenv>=1.0.0
12
+ Dynamic: license-file
13
+
14
+ Package managed by MseeP.ai
@@ -0,0 +1,174 @@
1
+ # ClaudePost
2
+
3
+ A Model Context Protocol (MCP) server that provides a seamless email management interface through Claude. This integration allows you to handle emails directly through natural language conversations with Claude, supporting features like searching, reading, and sending emails securely.
4
+
5
+ ## Features & Demo
6
+
7
+ ### Email Search and Reading
8
+
9
+ <p align="center">
10
+ <img src="assets/gif1.gif" width="800"/>
11
+ </p>
12
+
13
+ - 📧 Search emails by date range and keywords
14
+ - 📅 View daily email statistics
15
+ - 📝 Read full email content with threading support
16
+
17
+ ### Email Composition and Sending
18
+
19
+ <p align="center">
20
+ <img src="assets/gif2.gif" width="800"/>
21
+ </p>
22
+
23
+ - ✉️ Send emails with CC recipients support
24
+ - 🔒 Secure email handling with TLS
25
+
26
+ ## Prerequisites
27
+
28
+ - Python 3.12 or higher
29
+ - A Gmail account (or other email provider)
30
+ - If using Gmail:
31
+ - Two-factor authentication enabled
32
+ - [App-specific password](https://support.google.com/mail/answer/185833?hl=en) generated
33
+ - Claude Desktop application
34
+
35
+ ## Setup
36
+
37
+ 1. Install uv:
38
+
39
+ ```bash
40
+ # MacOS/Linux
41
+ curl -LsSf https://astral.sh/uv/install.sh | sh
42
+
43
+ # Remember to restart your terminal after installation
44
+ ```
45
+
46
+ 2. Clone and set up the project:
47
+
48
+ ```bash
49
+ # Clone the repository
50
+ git clone https://github.com/ZilongXue/claude-post.git
51
+ cd claude-post
52
+
53
+ # Create and activate virtual environment
54
+ uv venv
55
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
56
+
57
+ # Install dependencies
58
+ uv pip install -e .
59
+ ```
60
+
61
+ 3. Create a `.env` file in the project root:
62
+
63
+ ```env
64
+ EMAIL_ADDRESS=your.email@gmail.com
65
+ EMAIL_PASSWORD=your-app-specific-password
66
+ IMAP_SERVER=imap.gmail.com
67
+ SMTP_SERVER=smtp.gmail.com
68
+ SMTP_PORT=587
69
+ ```
70
+
71
+ 4. Configure Claude Desktop:
72
+
73
+ First, make sure you have Claude for Desktop installed. You can install the latest version [here](https://claude.ai/download). If you already have Claude for Desktop, make sure it's updated to the latest version.
74
+
75
+ Open your Claude Desktop configuration file:
76
+
77
+ ```bash
78
+ # MacOS
79
+ ~/Library/Application Support/Claude/claude_desktop_config.json
80
+
81
+ # Create the file if it doesn't exist
82
+ mkdir -p ~/Library/Application\ Support/Claude
83
+ touch ~/Library/Application\ Support/Claude/claude_desktop_config.json
84
+ ```
85
+
86
+ Add the following configuration:
87
+
88
+ ```json
89
+ {
90
+ "mcpServers": {
91
+ "email": {
92
+ "command": "/Users/username/.local/bin/uv",
93
+ "args": [
94
+ "--directory",
95
+ "/path/to/claude-post/src/email_client",
96
+ "run",
97
+ "email-client"
98
+ ]
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ Replace `/Users/username` and `/path/to/claude-post` with your actual paths.
105
+
106
+ After updating the configuration, restart Claude Desktop for the changes to take effect.
107
+
108
+ ## Running the Server
109
+
110
+ The server runs automatically through Claude Desktop:
111
+
112
+ - The server will start when Claude launches if configured correctly
113
+ - No manual server management needed
114
+ - Server stops when Claude is closed
115
+
116
+ ## Usage Through Claude
117
+
118
+ You can interact with your emails using natural language commands. Here are some examples:
119
+
120
+ ### Search Emails
121
+
122
+ - "Show me emails from last week"
123
+ - "Find emails with subject containing 'meeting'"
124
+ - "Search for emails from recruiting@linkedin.com between 2024-01-01 and 2024-01-07"
125
+ - "Search sent emails from last month"
126
+
127
+ ### Read Email Content
128
+
129
+ - "Show me the content of email #12345"
130
+ - "What's the full message of the last email from HR?"
131
+
132
+ ### Email Statistics
133
+
134
+ - "How many emails did I receive today?"
135
+ - "Show me daily email counts for the past week"
136
+
137
+ ### Send Emails
138
+
139
+ - "I want to send an email to john@example.com"
140
+ - "Send a meeting confirmation to team@company.com"
141
+
142
+ Note: For security reasons, Claude will always show you the email details for confirmation before actually sending.
143
+
144
+ ## Project Structure
145
+
146
+ ```
147
+ claude-post/
148
+ ├── pyproject.toml
149
+ ├── README.md
150
+ ├── LICENSE
151
+ ├── .env # Not included in repo
152
+ ├── .python-version # Python version specification
153
+ └── src/
154
+ └── email_client/
155
+ ├── __init__.py
156
+ ├── __main__.py
157
+ └── server.py # Main implementation
158
+ ```
159
+
160
+ ## Security Notes
161
+
162
+ - Use app-specific passwords instead of your main account password
163
+ - For Gmail users:
164
+ 1. Enable 2-Step Verification in your Google Account
165
+ 2. Generate an App Password for this application
166
+ 3. Use the App Password in your `.env` file
167
+
168
+ ## Logging
169
+
170
+ The application logs detailed information to `email_client.log`. Check this file for debugging information and error messages.
171
+
172
+ ## License
173
+
174
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "mseep-email_client"
3
+ version = "0.1.1"
4
+ description = "Email search and management tool using MCP protocol"
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "httpx>=0.28.1",
8
+ "mcp>=1.1.2",
9
+ "python-dotenv>=1.0.0",
10
+ ]
11
+ authors = [
12
+ { name = "mseep", email = "support@skydeck.ai" },
13
+ ]
14
+
15
+ [project.readme]
16
+ content-type = "text/plain"
17
+ text = "Package managed by MseeP.ai"
18
+
19
+ [project.scripts]
20
+ email-client = "email_client:main"
21
+
22
+ [build-system]
23
+ requires = [
24
+ "setuptools>=61.0",
25
+ "wheel",
26
+ ]
27
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ from . import server
2
+ import asyncio
3
+
4
+ def main():
5
+ """Main entry point for the package."""
6
+ asyncio.run(server.main())
7
+
8
+ __all__ = ['main', 'server']
@@ -0,0 +1,482 @@
1
+ from typing import Any
2
+ import asyncio
3
+ from datetime import datetime, timedelta
4
+ import email
5
+ import imaplib
6
+ import smtplib
7
+ import logging
8
+ from email.mime.text import MIMEText
9
+ from email.mime.multipart import MIMEMultipart
10
+ import os
11
+ from dotenv import load_dotenv
12
+ from mcp.server.models import InitializationOptions
13
+ import mcp.types as types
14
+ from mcp.server import NotificationOptions, Server
15
+ import mcp.server.stdio
16
+
17
+ # Configure logging
18
+ logging.basicConfig(
19
+ level=logging.DEBUG,
20
+ format='%(asctime)s - %(levelname)s - %(message)s',
21
+ filename='email_client.log'
22
+ )
23
+
24
+ # Load environment variables from .env file
25
+ load_dotenv()
26
+
27
+ # Email configuration
28
+ EMAIL_CONFIG = {
29
+ "email": os.getenv("EMAIL_ADDRESS", "your.email@gmail.com"),
30
+ "password": os.getenv("EMAIL_PASSWORD", "your-app-specific-password"),
31
+ "imap_server": os.getenv("IMAP_SERVER", "imap.gmail.com"),
32
+ "smtp_server": os.getenv("SMTP_SERVER", "smtp.gmail.com"),
33
+ "smtp_port": int(os.getenv("SMTP_PORT", "587"))
34
+ }
35
+
36
+ # Constants
37
+ SEARCH_TIMEOUT = 60 # seconds
38
+ MAX_EMAILS = 100
39
+
40
+ server = Server("email")
41
+
42
+ def format_email_summary(msg_data: tuple) -> dict:
43
+ """Format an email message into a summary dict with basic information."""
44
+ email_body = email.message_from_bytes(msg_data[0][1])
45
+
46
+ return {
47
+ "id": msg_data[0][0].split()[0].decode(), # Get the email ID
48
+ "from": email_body.get("From", "Unknown"),
49
+ "date": email_body.get("Date", "Unknown"),
50
+ "subject": email_body.get("Subject", "No Subject"),
51
+ }
52
+
53
+ def format_email_content(msg_data: tuple) -> dict:
54
+ """Format an email message into a dict with full content."""
55
+ email_body = email.message_from_bytes(msg_data[0][1])
56
+
57
+ # Extract body content
58
+ body = ""
59
+ if email_body.is_multipart():
60
+ # Handle multipart messages
61
+ for part in email_body.walk():
62
+ if part.get_content_type() == "text/plain":
63
+ body = part.get_payload(decode=True).decode()
64
+ break
65
+ elif part.get_content_type() == "text/html":
66
+ # If no plain text found, use HTML content
67
+ if not body:
68
+ body = part.get_payload(decode=True).decode()
69
+ else:
70
+ # Handle non-multipart messages
71
+ body = email_body.get_payload(decode=True).decode()
72
+
73
+ return {
74
+ "from": email_body.get("From", "Unknown"),
75
+ "to": email_body.get("To", "Unknown"),
76
+ "date": email_body.get("Date", "Unknown"),
77
+ "subject": email_body.get("Subject", "No Subject"),
78
+ "content": body
79
+ }
80
+
81
+ async def search_emails_async(mail: imaplib.IMAP4_SSL, search_criteria: str) -> list[dict]:
82
+ """Asynchronously search emails with timeout."""
83
+ loop = asyncio.get_event_loop()
84
+ try:
85
+ _, messages = await loop.run_in_executor(None, lambda: mail.search(None, search_criteria))
86
+ if not messages[0]:
87
+ return []
88
+
89
+ email_list = []
90
+ for num in messages[0].split()[:MAX_EMAILS]: # Limit to MAX_EMAILS
91
+ _, msg_data = await loop.run_in_executor(None, lambda: mail.fetch(num, '(RFC822)'))
92
+ email_list.append(format_email_summary(msg_data))
93
+
94
+ return email_list
95
+ except Exception as e:
96
+ raise Exception(f"Error searching emails: {str(e)}")
97
+
98
+ async def get_email_content_async(mail: imaplib.IMAP4_SSL, email_id: str) -> dict:
99
+ """Asynchronously get full content of a specific email."""
100
+ loop = asyncio.get_event_loop()
101
+ try:
102
+ _, msg_data = await loop.run_in_executor(None, lambda: mail.fetch(email_id, '(RFC822)'))
103
+ return format_email_content(msg_data)
104
+ except Exception as e:
105
+ raise Exception(f"Error fetching email content: {str(e)}")
106
+
107
+ async def count_emails_async(mail: imaplib.IMAP4_SSL, search_criteria: str) -> int:
108
+ """Asynchronously count emails matching the search criteria."""
109
+ loop = asyncio.get_event_loop()
110
+ try:
111
+ _, messages = await loop.run_in_executor(None, lambda: mail.search(None, search_criteria))
112
+ return len(messages[0].split()) if messages[0] else 0
113
+ except Exception as e:
114
+ raise Exception(f"Error counting emails: {str(e)}")
115
+
116
+ async def send_email_async(
117
+ to_addresses: list[str],
118
+ subject: str,
119
+ content: str,
120
+ cc_addresses: list[str] | None = None
121
+ ) -> None:
122
+ """Asynchronously send an email."""
123
+ try:
124
+ # Create message
125
+ msg = MIMEMultipart()
126
+ msg['From'] = EMAIL_CONFIG["email"]
127
+ msg['To'] = ', '.join(to_addresses)
128
+ if cc_addresses:
129
+ msg['Cc'] = ', '.join(cc_addresses)
130
+ msg['Subject'] = subject
131
+
132
+ # Add body
133
+ msg.attach(MIMEText(content, 'plain', 'utf-8'))
134
+
135
+ # Connect to SMTP server and send email
136
+ def send_sync():
137
+ with smtplib.SMTP(EMAIL_CONFIG["smtp_server"], EMAIL_CONFIG["smtp_port"]) as server:
138
+ server.set_debuglevel(1) # Enable debug output
139
+ logging.debug(f"Connecting to {EMAIL_CONFIG['smtp_server']}:{EMAIL_CONFIG['smtp_port']}")
140
+
141
+ # Start TLS
142
+ logging.debug("Starting TLS")
143
+ server.starttls()
144
+
145
+ # Login
146
+ logging.debug(f"Logging in as {EMAIL_CONFIG['email']}")
147
+ server.login(EMAIL_CONFIG["email"], EMAIL_CONFIG["password"])
148
+
149
+ # Send email
150
+ all_recipients = to_addresses + (cc_addresses or [])
151
+ logging.debug(f"Sending email to: {all_recipients}")
152
+ result = server.send_message(msg, EMAIL_CONFIG["email"], all_recipients)
153
+
154
+ if result:
155
+ # send_message returns a dict of failed recipients
156
+ raise Exception(f"Failed to send to some recipients: {result}")
157
+
158
+ logging.debug("Email sent successfully")
159
+
160
+ # Run the synchronous send function in the executor
161
+ loop = asyncio.get_event_loop()
162
+ await loop.run_in_executor(None, send_sync)
163
+
164
+ except Exception as e:
165
+ logging.error(f"Error in send_email_async: {str(e)}")
166
+ raise
167
+
168
+ @server.list_tools()
169
+ async def handle_list_tools() -> list[types.Tool]:
170
+ """
171
+ List available tools.
172
+ Each tool specifies its arguments using JSON Schema validation.
173
+ """
174
+ return [
175
+ types.Tool(
176
+ name="search-emails",
177
+ description="Search emails within a date range and/or with specific keywords",
178
+ inputSchema={
179
+ "type": "object",
180
+ "properties": {
181
+ "start_date": {
182
+ "type": "string",
183
+ "description": "Start date in YYYY-MM-DD format (optional)",
184
+ },
185
+ "end_date": {
186
+ "type": "string",
187
+ "description": "End date in YYYY-MM-DD format (optional)",
188
+ },
189
+ "keyword": {
190
+ "type": "string",
191
+ "description": "Keyword to search in email subject and body (optional)",
192
+ },
193
+ "folder": {
194
+ "type": "string",
195
+ "description": "Folder to search in ('inbox' or 'sent', defaults to 'inbox')",
196
+ "enum": ["inbox", "sent"],
197
+ },
198
+ },
199
+ },
200
+ ),
201
+ types.Tool(
202
+ name="get-email-content",
203
+ description="Get the full content of a specific email by its ID",
204
+ inputSchema={
205
+ "type": "object",
206
+ "properties": {
207
+ "email_id": {
208
+ "type": "string",
209
+ "description": "The ID of the email to retrieve",
210
+ },
211
+ },
212
+ "required": ["email_id"],
213
+ },
214
+ ),
215
+ types.Tool(
216
+ name="count-daily-emails",
217
+ description="Count emails received for each day in a date range",
218
+ inputSchema={
219
+ "type": "object",
220
+ "properties": {
221
+ "start_date": {
222
+ "type": "string",
223
+ "description": "Start date in YYYY-MM-DD format",
224
+ },
225
+ "end_date": {
226
+ "type": "string",
227
+ "description": "End date in YYYY-MM-DD format",
228
+ },
229
+ },
230
+ "required": ["start_date", "end_date"],
231
+ },
232
+ ),
233
+ types.Tool(
234
+ name="send-email",
235
+ description="CONFIRMATION STEP: Actually send the email after user confirms the details. Before calling this, first show the email details to the user for confirmation. Required fields: recipients (to), subject, and content. Optional: CC recipients.",
236
+ inputSchema={
237
+ "type": "object",
238
+ "properties": {
239
+ "to": {
240
+ "type": "array",
241
+ "items": {"type": "string"},
242
+ "description": "List of recipient email addresses (confirmed)",
243
+ },
244
+ "subject": {
245
+ "type": "string",
246
+ "description": "Confirmed email subject",
247
+ },
248
+ "content": {
249
+ "type": "string",
250
+ "description": "Confirmed email content",
251
+ },
252
+ "cc": {
253
+ "type": "array",
254
+ "items": {"type": "string"},
255
+ "description": "List of CC recipient email addresses (optional, confirmed)",
256
+ },
257
+ },
258
+ "required": ["to", "subject", "content"],
259
+ },
260
+ ),
261
+ ]
262
+
263
+ @server.call_tool()
264
+ async def handle_call_tool(
265
+ name: str, arguments: dict | None
266
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
267
+ """
268
+ Handle tool execution requests.
269
+ Tools can search emails and return results.
270
+ """
271
+ if not arguments:
272
+ arguments = {}
273
+
274
+ try:
275
+ if name == "send-email":
276
+ to_addresses = arguments.get("to", [])
277
+ subject = arguments.get("subject", "")
278
+ content = arguments.get("content", "")
279
+ cc_addresses = arguments.get("cc", [])
280
+
281
+ if not to_addresses:
282
+ return [types.TextContent(
283
+ type="text",
284
+ text="At least one recipient email address is required."
285
+ )]
286
+
287
+ try:
288
+ logging.info("Attempting to send email")
289
+ logging.info(f"To: {to_addresses}")
290
+ logging.info(f"Subject: {subject}")
291
+ logging.info(f"CC: {cc_addresses}")
292
+
293
+ async with asyncio.timeout(SEARCH_TIMEOUT):
294
+ await send_email_async(to_addresses, subject, content, cc_addresses)
295
+ return [types.TextContent(
296
+ type="text",
297
+ text="Email sent successfully! Check email_client.log for detailed logs."
298
+ )]
299
+ except asyncio.TimeoutError:
300
+ logging.error("Operation timed out while sending email")
301
+ return [types.TextContent(
302
+ type="text",
303
+ text="Operation timed out while sending email."
304
+ )]
305
+ except Exception as e:
306
+ error_msg = str(e)
307
+ logging.error(f"Failed to send email: {error_msg}")
308
+ return [types.TextContent(
309
+ type="text",
310
+ text=f"Failed to send email: {error_msg}\n\nPlease check:\n1. Email and password are correct in .env\n2. SMTP settings are correct\n3. Less secure app access is enabled (for Gmail)\n4. Using App Password if 2FA is enabled"
311
+ )]
312
+
313
+ # Connect to IMAP server using predefined credentials
314
+ mail = imaplib.IMAP4_SSL(EMAIL_CONFIG["imap_server"])
315
+ mail.login(EMAIL_CONFIG["email"], EMAIL_CONFIG["password"])
316
+
317
+ if name == "search-emails":
318
+ # 选择文件夹
319
+ folder = arguments.get("folder", "inbox") # 默认选择收件箱
320
+ if folder == "sent":
321
+ mail.select('"[Gmail]/Sent Mail"') # 对于 Gmail
322
+ else:
323
+ mail.select("inbox")
324
+
325
+ # Get optional parameters
326
+ start_date = arguments.get("start_date")
327
+ end_date = arguments.get("end_date")
328
+ keyword = arguments.get("keyword")
329
+
330
+ # If no dates provided, default to last 7 days
331
+ if not start_date:
332
+ start_date = datetime.now() - timedelta(days=7)
333
+ start_date = start_date.strftime("%d-%b-%Y")
334
+ else:
335
+ start_date = datetime.strptime(start_date, "%Y-%m-%d").strftime("%d-%b-%Y")
336
+
337
+ if not end_date:
338
+ end_date = datetime.now().strftime("%d-%b-%Y")
339
+ else:
340
+ # Convert end_date to datetime object once
341
+ end_date_obj = datetime.strptime(end_date, "%Y-%m-%d")
342
+ end_date = end_date_obj.strftime("%d-%b-%Y")
343
+
344
+ # Build search criteria
345
+ if start_date == end_date:
346
+ # If searching for a single day
347
+ search_criteria = f'ON "{start_date}"'
348
+ else:
349
+ # Calculate next day using the already converted end_date_obj
350
+ next_day = (end_date_obj + timedelta(days=1)).strftime("%d-%b-%Y")
351
+ search_criteria = f'SINCE "{start_date}" BEFORE "{next_day}"'
352
+
353
+ if keyword:
354
+ # Fix: Properly combine keyword search with date criteria
355
+ keyword_criteria = f'(OR SUBJECT "{keyword}" BODY "{keyword}")'
356
+ search_criteria = f'({keyword_criteria} {search_criteria})'
357
+
358
+ logging.debug(f"Search criteria: {search_criteria}") # Add debug logging
359
+
360
+ try:
361
+ async with asyncio.timeout(SEARCH_TIMEOUT):
362
+ email_list = await search_emails_async(mail, search_criteria)
363
+
364
+ if not email_list:
365
+ return [types.TextContent(
366
+ type="text",
367
+ text="No emails found matching the criteria."
368
+ )]
369
+
370
+ # Format the results as a table
371
+ result_text = "Found emails:\n\n"
372
+ result_text += "ID | From | Date | Subject\n"
373
+ result_text += "-" * 80 + "\n"
374
+
375
+ for email in email_list:
376
+ result_text += f"{email['id']} | {email['from']} | {email['date']} | {email['subject']}\n"
377
+
378
+ result_text += "\nUse get-email-content with an email ID to view the full content of a specific email."
379
+
380
+ return [types.TextContent(
381
+ type="text",
382
+ text=result_text
383
+ )]
384
+
385
+ except asyncio.TimeoutError:
386
+ return [types.TextContent(
387
+ type="text",
388
+ text="Search operation timed out. Please try with a more specific search criteria."
389
+ )]
390
+
391
+ elif name == "get-email-content":
392
+ email_id = arguments.get("email_id")
393
+ if not email_id:
394
+ return [types.TextContent(
395
+ type="text",
396
+ text="Email ID is required."
397
+ )]
398
+
399
+ try:
400
+ async with asyncio.timeout(SEARCH_TIMEOUT):
401
+ email_content = await get_email_content_async(mail, email_id)
402
+
403
+ result_text = (
404
+ f"From: {email_content['from']}\n"
405
+ f"To: {email_content['to']}\n"
406
+ f"Date: {email_content['date']}\n"
407
+ f"Subject: {email_content['subject']}\n"
408
+ f"\nContent:\n{email_content['content']}"
409
+ )
410
+
411
+ return [types.TextContent(
412
+ type="text",
413
+ text=result_text
414
+ )]
415
+
416
+ except asyncio.TimeoutError:
417
+ return [types.TextContent(
418
+ type="text",
419
+ text="Operation timed out while fetching email content."
420
+ )]
421
+
422
+ elif name == "count-daily-emails":
423
+ start_date = datetime.strptime(arguments["start_date"], "%Y-%m-%d")
424
+ end_date = datetime.strptime(arguments["end_date"], "%Y-%m-%d")
425
+
426
+ result_text = "Daily email counts:\n\n"
427
+ result_text += "Date | Count\n"
428
+ result_text += "-" * 30 + "\n"
429
+
430
+ current_date = start_date
431
+ while current_date <= end_date:
432
+ date_str = current_date.strftime("%d-%b-%Y")
433
+ search_criteria = f'(ON "{date_str}")'
434
+
435
+ try:
436
+ async with asyncio.timeout(SEARCH_TIMEOUT):
437
+ count = await count_emails_async(mail, search_criteria)
438
+ result_text += f"{current_date.strftime('%Y-%m-%d')} | {count}\n"
439
+ except asyncio.TimeoutError:
440
+ result_text += f"{current_date.strftime('%Y-%m-%d')} | Timeout\n"
441
+
442
+ current_date += timedelta(days=1)
443
+
444
+ return [types.TextContent(
445
+ type="text",
446
+ text=result_text
447
+ )]
448
+
449
+ else:
450
+ raise ValueError(f"Unknown tool: {name}")
451
+
452
+ except Exception as e:
453
+ return [types.TextContent(
454
+ type="text",
455
+ text=f"Error: {str(e)}"
456
+ )]
457
+ finally:
458
+ try:
459
+ mail.close()
460
+ mail.logout()
461
+ except:
462
+ pass
463
+
464
+ async def main():
465
+ # Run the server using stdin/stdout streams
466
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
467
+ await server.run(
468
+ read_stream,
469
+ write_stream,
470
+ InitializationOptions(
471
+ server_name="email",
472
+ server_version="0.1.0",
473
+ capabilities=server.get_capabilities(
474
+ notification_options=NotificationOptions(),
475
+ experimental_capabilities={},
476
+ ),
477
+ ),
478
+ )
479
+
480
+ if __name__ == "__main__":
481
+ asyncio.run(main())
482
+
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: mseep-email_client
3
+ Version: 0.1.1
4
+ Summary: Email search and management tool using MCP protocol
5
+ Author-email: mseep <support@skydeck.ai>
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/plain
8
+ License-File: LICENSE
9
+ Requires-Dist: httpx>=0.28.1
10
+ Requires-Dist: mcp>=1.1.2
11
+ Requires-Dist: python-dotenv>=1.0.0
12
+ Dynamic: license-file
13
+
14
+ Package managed by MseeP.ai
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/email_client/__init__.py
5
+ src/email_client/server.py
6
+ src/mseep_email_client.egg-info/PKG-INFO
7
+ src/mseep_email_client.egg-info/SOURCES.txt
8
+ src/mseep_email_client.egg-info/dependency_links.txt
9
+ src/mseep_email_client.egg-info/entry_points.txt
10
+ src/mseep_email_client.egg-info/requires.txt
11
+ src/mseep_email_client.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ email-client = email_client:main
@@ -0,0 +1,3 @@
1
+ httpx>=0.28.1
2
+ mcp>=1.1.2
3
+ python-dotenv>=1.0.0