#
tokens: 11185/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── .python-version
├── demo.mp4
├── LICENSE
├── pyproject.toml
├── README.md
├── requirements.txt
├── src
│   └── gmail_plugin
│       ├── __init__.py
│       └── server.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
1 | 3.12
2 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Python-generated files
 2 | __pycache__/
 3 | *.py[oc]
 4 | build/
 5 | dist/
 6 | wheels/
 7 | *.egg-info
 8 | 
 9 | # Virtual environments
10 | .venv
11 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Gmail Plugin MCP Server
  2 | 
  3 | [![Python Version](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
  4 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
  5 | 
  6 | A powerful MCP server that enables Gmail integration, allowing you to manage emails directly through MCP clients. This plugin provides seamless access to Gmail's core functionality including reading, sending, and managing emails.
  7 | 
  8 | > **Reference**: For a sample MCP server implementation using uvx, check out [this example](https://github.com/modelcontextprotocol/uvx/tree/main/examples/sample-mcp-server).
  9 | 
 10 | ## 🎥 Demo
 11 | 
 12 | https://github.com/user-attachments/assets/df9e86cf-1f6b-4265-9c68-b3ed88103d1f
 13 | 
 14 | ## ✨ Features
 15 | 
 16 | - 📧 Send and receive emails
 17 | - 📥 Read unread messages
 18 | - 🗑️ Trash emails
 19 | - 📱 Open emails in browser
 20 | - 📝 Mark emails as read
 21 | - 🔒 Secure OAuth2 authentication
 22 | 
 23 | ## 🚀 Quick Start
 24 | 
 25 | ### Prerequisites
 26 | 
 27 | - Python 3.12 or higher
 28 | - Gmail API credentials
 29 | - MCP client (like Claude Desktop)
 30 | 
 31 | ### Installation
 32 | 
 33 | 1. Clone the repository:
 34 | ```bash
 35 | git clone https://github.com/yourusername/gmail-plugin.git
 36 | cd gmail-plugin
 37 | ```
 38 | 
 39 | 2. Install dependencies (choose one method):
 40 | 
 41 | ```bash
 42 | # Method 1: Install in editable mode
 43 | uv pip install -e .
 44 | 
 45 | # Method 2: Install using requirements.txt
 46 | uv pip install -r requirements.txt
 47 | 
 48 | # Method 3: Install using uv sync (recommended)
 49 | uv sync --dev --all-extras
 50 | ```
 51 | 
 52 | 3. Configure your Gmail API credentials:
 53 |    - Go to [Google Cloud Console](https://console.cloud.google.com)
 54 |    - Create a new project or select existing one
 55 |    - Enable Gmail API
 56 |    - Configure OAuth consent screen:
 57 |      - Select "External" user type (no publishing required)
 58 |      - Go to the Audiences tab : Add your email as a "Test user"
 59 |      - Add OAuth scope: `https://www.googleapis.com/auth/gmail/modify` 
 60 |    - Create OAuth 2.0 credentials:
 61 |      - Choose "Desktop App" as application type
 62 |      - Download the JSON credentials file  
 63 |    - Save the credentials file and note its absolute path (will be used for `--creds-file-path`)
 64 | 
 65 | ### Configuration
 66 | 
 67 | #### For Development/Unpublished Servers
 68 | 
 69 | Add this to your MCP client configuration:
 70 | 
 71 | ```json
 72 | "mcpServers": {
 73 |   "gmail-plugin": {
 74 |     "command": "uv",
 75 |     "args": [
 76 |       "--directory",
 77 |       "[absolute path to working directory]",
 78 |       "run",
 79 |       "server.py"
 80 |       "--creds-file-path",
 81 |       "[absolute-path-to-credentials-file]",
 82 |       "--token-path",
 83 |       "[absolute-path-to-access-tokens-file]"
 84 |     ]
 85 |   }
 86 | }
 87 | ```
 88 | 
 89 | #### For Published Servers
 90 | 
 91 | ```json
 92 | "mcpServers": {
 93 |   "gmail-plugin": {
 94 |     "command": "uvx",
 95 |     "args": [
 96 |       "gmail-plugin"
 97 |     ]
 98 |   }
 99 | }
100 | ```
101 | 
102 | ## 🛠️ Development
103 | 
104 | ### Building and Publishing
105 | 
106 | 1. Sync dependencies:
107 | ```bash
108 | uv sync
109 | ```
110 | 
111 | 2. Build package:
112 | ```bash
113 | uv build
114 | ```
115 | 
116 | 3. Publish to PyPI:
117 | ```bash
118 | uv publish
119 | ```
120 | 
121 | ### Debugging
122 | 
123 | Use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) for debugging:
124 | 
125 | ```bash
126 | npx @modelcontextprotocol/inspector uv --directory C:\Users\sanch\Desktop\gmail_plugin\gmail-plugin run gmail-plugin
127 | ```
128 | 
129 | ## 📚 API Reference
130 | 
131 | ### Available Tools
132 | 
133 | | Tool Name | Description | Required Arguments |
134 | |-----------|-------------|-------------------|
135 | | `send-email` | Send an email | recipient_id, subject, message |
136 | | `get-unread-emails` | Retrieve unread emails | None |
137 | | `read-email` | Read email content | email_id |
138 | | `trash-email` | Move email to trash | email_id |
139 | | `mark-email-as-read` | Mark email as read | email_id |
140 | | `open-email` | Open email in browser | email_id |
141 | 
142 | ### Available Prompts
143 | 
144 | | Prompt Name | Description | Arguments |
145 | |-------------|-------------|-----------|
146 | | `manage-email` | Act as email administrator | None |
147 | | `draft-email` | Draft a new email | content, recipient, recipient_email |
148 | | `edit-draft` | Edit existing email draft | changes, current_draft |
149 | 
150 | ## 🤝 Contributing
151 | 
152 | Contributions are welcome! Please feel free to submit a Pull Request.
153 | 
154 | ## 📄 License
155 | 
156 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
157 | 
```

--------------------------------------------------------------------------------
/src/gmail_plugin/__init__.py:
--------------------------------------------------------------------------------

```python
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 | # Optionally expose other important items at package level
9 | __all__ = ['main', 'server']
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "gmail-plugin"
 3 | version = "1.0"
 4 | description = "A tool to enable access to gmail tools in MCP clients"
 5 | readme = "README.md"
 6 | requires-python = ">=3.12"
 7 | dependencies = [
 8 |     "mcp>=1.5.0",
 9 |     "google-api-python-client>=2.156.0",
10 |     "google-auth-httplib2>=0.2.0",
11 |     "google-auth-oauthlib>=1.2.1",
12 |     "aiohttp>=3.9.0"
13 | ]
14 | [[project.authors]]
15 | email = "[email protected]"
16 | 
17 | [build-system]
18 | requires = [ "hatchling",]
19 | build-backend = "hatchling.build"
20 | 
21 | [project.scripts]
22 | gmail-plugin = "gmail_plugin:main"
23 | 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
 1 | aiohappyeyeballs==2.6.1
 2 | aiohttp==3.11.14
 3 | aiosignal==1.3.2
 4 | annotated-types==0.7.0
 5 | anyio==4.9.0
 6 | attrs==25.3.0
 7 | cachetools==5.5.2
 8 | certifi==2025.1.31
 9 | charset-normalizer==3.4.1
10 | click==8.1.8
11 | colorama==0.4.6
12 | frozenlist==1.5.0
13 | google-api-core==2.24.2
14 | google-api-python-client==2.165.0
15 | google-auth==2.38.0
16 | google-auth-httplib2==0.2.0
17 | google-auth-oauthlib==1.2.1
18 | googleapis-common-protos==1.69.2
19 | h11==0.14.0
20 | httpcore==1.0.7
21 | httplib2==0.22.0
22 | httpx==0.28.1
23 | httpx-sse==0.4.0
24 | idna==3.10
25 | mcp==1.5.0
26 | multidict==6.2.0
27 | oauthlib==3.2.2
28 | propcache==0.3.0
29 | proto-plus==1.26.1
30 | protobuf==6.30.1
31 | pyasn1==0.6.1
32 | pyasn1-modules==0.4.1
33 | pydantic==2.10.6
34 | pydantic-core==2.27.2
35 | pydantic-settings==2.8.1
36 | pyparsing==3.2.1
37 | python-dotenv==1.0.1
38 | requests==2.32.3
39 | requests-oauthlib==2.0.0
40 | rsa==4.9
41 | sniffio==1.3.1
42 | sse-starlette==2.2.1
43 | starlette==0.46.1
44 | typing-extensions==4.12.2
45 | uritemplate==4.1.1
46 | urllib3==2.3.0
47 | uvicorn==0.34.0
48 | yarl==1.18.3
49 | 
```

--------------------------------------------------------------------------------
/src/gmail_plugin/server.py:
--------------------------------------------------------------------------------

```python
  1 | from typing import Any
  2 | import argparse
  3 | import os
  4 | import asyncio
  5 | import logging
  6 | import base64
  7 | from email.message import EmailMessage
  8 | from email.header import decode_header
  9 | from base64 import urlsafe_b64decode
 10 | from email import message_from_bytes
 11 | import webbrowser
 12 | 
 13 | from mcp.server.models import InitializationOptions
 14 | import mcp.types as types
 15 | from mcp.server import NotificationOptions, Server
 16 | import mcp.server.stdio
 17 | 
 18 | 
 19 | from google.auth.transport.requests import Request
 20 | from google.oauth2.credentials import Credentials
 21 | from google_auth_oauthlib.flow import InstalledAppFlow
 22 | from googleapiclient.discovery import build
 23 | from googleapiclient.errors import HttpError
 24 | 
 25 | 
 26 | # Configure logging
 27 | logging.basicConfig(level=logging.INFO)
 28 | logger = logging.getLogger(__name__)
 29 | 
 30 | EMAIL_ADMIN_PROMPTS = """You are an email administrator. 
 31 | You can draft, edit, read, trash, open, and send emails.
 32 | You've been given access to a specific gmail account. 
 33 | You have the following tools available:
 34 | - Send an email (send-email)
 35 | - Retrieve unread emails (get-unread-emails)
 36 | - Read email content (read-email)
 37 | - Trash email (tras-email)
 38 | - Open email in browser (open-email)
 39 | Never send an email draft or trash an email unless the user confirms first. 
 40 | Always ask for approval if not already given.
 41 | """
 42 | 
 43 | # Define available prompts
 44 | PROMPTS = {
 45 |     "manage-email": types.Prompt(
 46 |         name="manage-email",
 47 |         description="Act like an email administator",
 48 |         arguments=None,
 49 |     ),
 50 |     "draft-email": types.Prompt(
 51 |         name="draft-email",
 52 |         description="Draft an email with cotent and recipient",
 53 |         arguments=[
 54 |             types.PromptArgument(
 55 |                 name="content",
 56 |                 description="What the email is about",
 57 |                 required=True
 58 |             ),
 59 |             types.PromptArgument(
 60 |                 name="recipient",
 61 |                 description="Who should the email be addressed to",
 62 |                 required=True
 63 |             ),
 64 |             types.PromptArgument(
 65 |                 name="recipient_email",
 66 |                 description="Recipient's email address",
 67 |                 required=True
 68 |             ),
 69 |         ],
 70 |     ),
 71 |     "edit-draft": types.Prompt(
 72 |         name="edit-draft",
 73 |         description="Edit the existing email draft",
 74 |         arguments=[
 75 |             types.PromptArgument(
 76 |                 name="changes",
 77 |                 description="What changes should be made to the draft",
 78 |                 required=True
 79 |             ),
 80 |             types.PromptArgument(
 81 |                 name="current_draft",
 82 |                 description="The current draft to edit",
 83 |                 required=True
 84 |             ),
 85 |         ],
 86 |     ),
 87 | }
 88 | 
 89 | 
 90 | def decode_mime_header(header: str) -> str: 
 91 |     """Helper function to decode encoded email headers"""
 92 |     
 93 |     decoded_parts = decode_header(header)
 94 |     decoded_string = ''
 95 |     for part, encoding in decoded_parts: 
 96 |         if isinstance(part, bytes): 
 97 |             # Decode bytes to string using the specified encoding 
 98 |             decoded_string += part.decode(encoding or 'utf-8') 
 99 |         else: 
100 |             # Already a string 
101 |             decoded_string += part 
102 |     return decoded_string
103 | 
104 | 
105 | class GmailService:
106 |     def __init__(self,
107 |                  creds_file_path: str,
108 |                  token_path: str ,
109 |                  scopes: list[str] = ['https://www.googleapis.com/auth/gmail.modify']):
110 |         logger.info(f"Initializing GmailService with creds file: {creds_file_path}")
111 |         self.creds_file_path = creds_file_path
112 |         self.token_path = token_path
113 |         self.scopes = scopes
114 |         self.token = self._get_token()
115 |         logger.info("Token retrieved successfully")
116 |         self.service = self._get_service()
117 |         logger.info("Gmail service initialized")
118 |         self.user_email = self._get_user_email()
119 |         logger.info(f"User email retrieved: {self.user_email}")
120 | 
121 |     def _get_token(self) -> Credentials:
122 |         """Get or refresh Google API token"""
123 | 
124 |         token = None
125 |     
126 |         if os.path.exists(self.token_path):
127 |             logger.info('Loading token from file')
128 |             token = Credentials.from_authorized_user_file(self.token_path, self.scopes)
129 | 
130 |         if not token or not token.valid:
131 |             if token and token.expired and token.refresh_token:
132 |                 logger.info('Refreshing token')
133 |                 token.refresh(Request())
134 |             else:
135 |                 logger.info('Fetching new token')
136 |                 flow = InstalledAppFlow.from_client_secrets_file(self.creds_file_path, self.scopes)
137 |                 token = flow.run_local_server(port=0)
138 | 
139 |             with open(self.token_path, 'w') as token_file:
140 |                 token_file.write(token.to_json())
141 |                 logger.info(f'Token saved to {self.token_path}')
142 | 
143 |         return token
144 | 
145 |     def _get_service(self) -> Any:
146 |         """Initialize Gmail API service"""
147 |         try:
148 |             service = build('gmail', 'v1', credentials=self.token)
149 |             return service
150 |         except HttpError as error:
151 |             logger.error(f'An error occurred building Gmail service: {error}')
152 |             raise ValueError(f'An error occurred: {error}')
153 |     
154 |     def _get_user_email(self) -> str:
155 |         """Get user email address"""
156 |         profile = self.service.users().getProfile(userId='me').execute()
157 |         user_email = profile.get('emailAddress', '')
158 |         return user_email
159 |     
160 |     async def send_email(self, recipient_id: str, subject: str, message: str,) -> dict:
161 |         """Creates and sends an email message"""
162 |         try:
163 |             message_obj = EmailMessage()
164 |             message_obj.set_content(message)
165 |             
166 |             message_obj['To'] = recipient_id
167 |             message_obj['From'] = self.user_email
168 |             message_obj['Subject'] = subject
169 | 
170 |             encoded_message = base64.urlsafe_b64encode(message_obj.as_bytes()).decode()
171 |             create_message = {'raw': encoded_message}
172 |             
173 |             send_message = await asyncio.to_thread(
174 |                 self.service.users().messages().send(userId="me", body=create_message).execute
175 |             )
176 |             logger.info(f"Message sent: {send_message['id']}")
177 |             return {"status": "success", "message_id": send_message["id"]}
178 |         except HttpError as error:
179 |             return {"status": "error", "error_message": str(error)}
180 | 
181 |     async def open_email(self, email_id: str) -> str:
182 |         """Opens email in browser given ID."""
183 |         try:
184 |             url = f"https://mail.google.com/#all/{email_id}"
185 |             webbrowser.open(url, new=0, autoraise=True)
186 |             return "Email opened in browser successfully."
187 |         except HttpError as error:
188 |             return f"An HttpError occurred: {str(error)}"
189 | 
190 |     async def get_unread_emails(self) -> list[dict]:
191 |         """
192 |         Retrieves unread messages from mailbox.
193 |         Returns a list of messages with their details.
194 |         """
195 |         try:
196 |             user_id = 'me'
197 |             query = 'in:inbox is:unread category:primary'
198 |             
199 |             # Get message IDs first
200 |             response = self.service.users().messages().list(
201 |                 userId=user_id,
202 |                 q=query,
203 |                 maxResults=10  # Limit to prevent timeout
204 |             ).execute()
205 |             
206 |             message_ids = []
207 |             if 'messages' in response:
208 |                 message_ids.extend(response['messages'])
209 |             
210 |             # Get actual message details
211 |             messages = []
212 |             for msg_id in message_ids:
213 |                 msg = self.service.users().messages().get(
214 |                     userId=user_id, 
215 |                     id=msg_id['id'],
216 |                     format='metadata',  # Use 'full' if you need the complete message
217 |                     metadataHeaders=['Subject', 'From', 'Date']
218 |                 ).execute()
219 |                 
220 |                 # Extract and format relevant information
221 |                 headers = msg.get('payload', {}).get('headers', [])
222 |                 email_data = {
223 |                     'id': msg['id'],
224 |                     'threadId': msg['threadId'],
225 |                     'subject': next((h['value'] for h in headers if h['name'] == 'Subject'), 'No Subject'),
226 |                     'from': next((h['value'] for h in headers if h['name'] == 'From'), 'Unknown'),
227 |                     'date': next((h['value'] for h in headers if h['name'] == 'Date'), 'Unknown'),
228 |                     'snippet': msg.get('snippet', '')
229 |                 }
230 |                 messages.append(email_data)
231 |                 
232 |             return messages
233 | 
234 |         except HttpError as error:
235 |             print(f"An HttpError occurred: {str(error)}")
236 |             return []
237 |         except Exception as e:
238 |             print(f"An unexpected error occurred: {str(e)}")
239 |             return []
240 | 
241 |     async def read_email(self, email_id: str) -> dict[str, str]| str:
242 |         """Retrieves email contents including to, from, subject, and contents."""
243 |         try:
244 |             msg = self.service.users().messages().get(userId="me", id=email_id, format='raw').execute()
245 |             email_metadata = {}
246 | 
247 |             # Decode the base64URL encoded raw content
248 |             raw_data = msg['raw']
249 |             decoded_data = urlsafe_b64decode(raw_data)
250 | 
251 |             # Parse the RFC 2822 email
252 |             mime_message = message_from_bytes(decoded_data)
253 | 
254 |             # Extract the email body
255 |             body = None
256 |             if mime_message.is_multipart():
257 |                 for part in mime_message.walk():
258 |                     # Extract the text/plain part
259 |                     if part.get_content_type() == "text/plain":
260 |                         body = part.get_payload(decode=True).decode()
261 |                         break
262 |             else:
263 |                 # For non-multipart messages
264 |                 body = mime_message.get_payload(decode=True).decode()
265 |             email_metadata['content'] = body
266 |             
267 |             # Extract metadata
268 |             email_metadata['subject'] = decode_mime_header(mime_message.get('subject', ''))
269 |             email_metadata['from'] = mime_message.get('from','')
270 |             email_metadata['to'] = mime_message.get('to','')
271 |             email_metadata['date'] = mime_message.get('date','')
272 |             
273 |             logger.info(f"Email read: {email_id}")
274 |             
275 |             # We want to mark email as read once we read it
276 |             await self.mark_email_as_read(email_id)
277 | 
278 |             return email_metadata
279 |         except HttpError as error:
280 |             return f"An HttpError occurred: {str(error)}"
281 |         
282 |         
283 |     async def trash_email(self, email_id: str) -> str:
284 |         """Moves email to trash given ID."""
285 |         try:
286 |             self.service.users().messages().trash(userId="me", id=email_id).execute()
287 |             logger.info(f"Email moved to trash: {email_id}")
288 |             return "Email moved to trash successfully."
289 |         except HttpError as error:
290 |             return f"An HttpError occurred: {str(error)}"
291 |         
292 |     async def mark_email_as_read(self, email_id: str) -> str:
293 |         """Marks email as read given ID."""
294 |         try:
295 |             self.service.users().messages().modify(userId="me", id=email_id, body={'removeLabelIds': ['UNREAD']}).execute()
296 |             logger.info(f"Email marked as read: {email_id}")
297 |             return "Email marked as read."
298 |         except HttpError as error:
299 |             return f"An HttpError occurred: {str(error)}"
300 |   
301 | async def main(creds_file_path: str,
302 |                token_path: str):
303 |     
304 |     gmail_service = GmailService(creds_file_path, token_path)
305 |     server = Server("gmail")
306 | 
307 |     @server.list_prompts()
308 |     async def list_prompts() -> list[types.Prompt]:
309 |         return list(PROMPTS.values())
310 | 
311 |     @server.get_prompt()
312 |     async def get_prompt(
313 |         name: str, arguments: dict[str, str] | None = None
314 |     ) -> types.GetPromptResult:
315 |         if name not in PROMPTS:
316 |             raise ValueError(f"Prompt not found: {name}")
317 | 
318 |         if name == "manage-email":
319 |             return types.GetPromptResult(
320 |                 messages=[
321 |                     types.PromptMessage(
322 |                         role="user",
323 |                         content=types.TextContent(
324 |                             type="text",
325 |                             text=EMAIL_ADMIN_PROMPTS,
326 |                         )
327 |                     )
328 |                 ]
329 |             )
330 | 
331 |         if name == "draft-email":
332 |             content = arguments.get("content", "")
333 |             recipient = arguments.get("recipient", "")
334 |             recipient_email = arguments.get("recipient_email", "")
335 |             
336 |             # First message asks the LLM to create the draft
337 |             return types.GetPromptResult(
338 |                 messages=[
339 |                     types.PromptMessage(
340 |                         role="user",
341 |                         content=types.TextContent(
342 |                             type="text",
343 |                             text=f"""Please draft an email about {content} for {recipient} ({recipient_email}).
344 |                             Include a subject line starting with 'Subject:' on the first line.
345 |                             Do not send the email yet, just draft it and ask the user for their thoughts."""
346 |                         )
347 |                     )
348 |                 ]
349 |             )
350 |         
351 |         elif name == "edit-draft":
352 |             changes = arguments.get("changes", "")
353 |             current_draft = arguments.get("current_draft", "")
354 |             
355 |             # Edit existing draft based on requested changes
356 |             return types.GetPromptResult(
357 |                 messages=[
358 |                     types.PromptMessage(
359 |                         role="user",
360 |                         content=types.TextContent(
361 |                             type="text",
362 |                             text=f"""Please revise the current email draft:
363 |                             {current_draft}
364 |                             
365 |                             Requested changes:
366 |                             {changes}
367 |                             
368 |                             Please provide the updated draft."""
369 |                         )
370 |                     )
371 |                 ]
372 |             )
373 | 
374 |         raise ValueError("Prompt implementation not found")
375 | 
376 |     @server.list_tools()
377 |     async def handle_list_tools() -> list[types.Tool]:
378 |         return [
379 |             types.Tool(
380 |                 name="send-email",
381 |                 description="""Sends email to recipient. 
382 |                 Do not use if user only asked to draft email. 
383 |                 Drafts must be approved before sending.""",
384 |                 inputSchema={
385 |                     "type": "object",
386 |                     "properties": {
387 |                         "recipient_id": {
388 |                             "type": "string",
389 |                             "description": "Recipient email address",
390 |                         },
391 |                         "subject": {
392 |                             "type": "string",
393 |                             "description": "Email subject",
394 |                         },
395 |                         "message": {
396 |                             "type": "string",
397 |                             "description": "Email content text",
398 |                         },
399 |                     },
400 |                     "required": ["recipient_id", "subject", "message"],
401 |                 },
402 |             ),
403 |             types.Tool(
404 |                 name="trash-email",
405 |                 description="""Moves email to trash. 
406 |                 Confirm before moving email to trash.""",
407 |                 inputSchema={
408 |                     "type": "object",
409 |                     "properties": {
410 |                         "email_id": {
411 |                             "type": "string",
412 |                             "description": "Email ID",
413 |                         },
414 |                     },
415 |                     "required": ["email_id"],
416 |                 },
417 |             ),
418 |             types.Tool(
419 |                 name="get-unread-emails",
420 |                 description="Retrieve unread emails",
421 |                 inputSchema={
422 |                     "type": "object",
423 |                     "properties": {},
424 |                     "required": []
425 |                 },
426 |             ),
427 |             types.Tool(
428 |                 name="read-email",
429 |                 description="Retrieves given email content",
430 |                 inputSchema={
431 |                     "type": "object",
432 |                     "properties": {
433 |                         "email_id": {
434 |                             "type": "string",
435 |                             "description": "Email ID",
436 |                         },
437 |                     },
438 |                     "required": ["email_id"],
439 |                 },
440 |             ),
441 |             types.Tool(
442 |                 name="mark-email-as-read",
443 |                 description="Marks given email as read",
444 |                 inputSchema={
445 |                     "type": "object",
446 |                     "properties": {
447 |                         "email_id": {
448 |                             "type": "string",
449 |                             "description": "Email ID",
450 |                         },
451 |                     },
452 |                     "required": ["email_id"],
453 |                 },
454 |             ),
455 |             types.Tool(
456 |                 name="open-email",
457 |                 description="Open email in browser",
458 |                 inputSchema={
459 |                     "type": "object",
460 |                     "properties": {
461 |                         "email_id": {
462 |                             "type": "string",
463 |                             "description": "Email ID",
464 |                         },
465 |                     },
466 |                     "required": ["email_id"],
467 |                 },
468 |             ),
469 |         ]
470 | 
471 |     @server.call_tool()
472 |     async def handle_call_tool(
473 |         name: str, arguments: dict | None
474 |     ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
475 | 
476 |         if name == "send-email":
477 |             recipient = arguments.get("recipient_id")
478 |             if not recipient:
479 |                 raise ValueError("Missing recipient parameter")
480 |             subject = arguments.get("subject")
481 |             if not subject:
482 |                 raise ValueError("Missing subject parameter")
483 |             message = arguments.get("message")
484 |             if not message:
485 |                 raise ValueError("Missing message parameter")
486 |                 
487 |             # Extract subject and message content
488 |             email_lines = message.split('\n')
489 |             if email_lines[0].startswith('Subject:'):
490 |                 subject = email_lines[0][8:].strip()
491 |                 message_content = '\n'.join(email_lines[1:]).strip()
492 |             else:
493 |                 message_content = message
494 |                 
495 |             send_response = await gmail_service.send_email(recipient, subject, message_content)
496 |             
497 |             if send_response["status"] == "success":
498 |                 response_text = f"Email sent successfully. Message ID: {send_response['message_id']}"
499 |             else:
500 |                 response_text = f"Failed to send email: {send_response['error_message']}"
501 |             return [types.TextContent(type="text", text=response_text)]
502 | 
503 |         if name == "get-unread-emails":
504 |                 
505 |             unread_emails = await gmail_service.get_unread_emails()
506 |             return [types.TextContent(type="text", text=str(unread_emails),artifact={"type": "json", "data": unread_emails} )]
507 |         
508 |         if name == "read-email":
509 |             email_id = arguments.get("email_id")
510 |             if not email_id:
511 |                 raise ValueError("Missing email ID parameter")
512 |                 
513 |             retrieved_email = await gmail_service.read_email(email_id)
514 |             return [types.TextContent(type="text", text=str(retrieved_email),artifact={"type": "dictionary", "data": retrieved_email} )]
515 |         if name == "open-email":
516 |             email_id = arguments.get("email_id")
517 |             if not email_id:
518 |                 raise ValueError("Missing email ID parameter")
519 |                 
520 |             msg = await gmail_service.open_email(email_id)
521 |             return [types.TextContent(type="text", text=str(msg))]
522 |         if name == "trash-email":
523 |             email_id = arguments.get("email_id")
524 |             if not email_id:
525 |                 raise ValueError("Missing email ID parameter")
526 |                 
527 |             msg = await gmail_service.trash_email(email_id)
528 |             return [types.TextContent(type="text", text=str(msg))]
529 |         if name == "mark-email-as-read":
530 |             email_id = arguments.get("email_id")
531 |             if not email_id:
532 |                 raise ValueError("Missing email ID parameter")
533 |                 
534 |             msg = await gmail_service.mark_email_as_read(email_id)
535 |             return [types.TextContent(type="text", text=str(msg))]
536 |         else:
537 |             logger.error(f"Unknown tool: {name}")
538 |             raise ValueError(f"Unknown tool: {name}")
539 | 
540 |     async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
541 |         await server.run(
542 |             read_stream,
543 |             write_stream,
544 |             InitializationOptions(
545 |                 server_name="gmail",
546 |                 server_version="0.1.0",
547 |                 capabilities=server.get_capabilities(
548 |                     notification_options=NotificationOptions(),
549 |                     experimental_capabilities={},
550 |                 ),
551 |             ),
552 |         )
553 | 
554 | if __name__ == "__main__":
555 |     parser = argparse.ArgumentParser(description='Gmail API MCP Server')
556 |     parser.add_argument('--creds-file-path',
557 |                         required=True,
558 |                        help='OAuth 2.0 credentials file path')
559 |     parser.add_argument('--token-path',
560 |                         required=True,
561 |                        help='File location to store and retrieve access and refresh tokens for application')
562 |     
563 |     args = parser.parse_args()
564 |     asyncio.run(main(args.creds_file_path, args.token_path))
565 | 
566 | #     # path = C:/Users/sanch/Desktop/gmail_plugin/gmail-plugin
567 | 
568 | #!/usr/bin/env python3
569 | # import os
570 | # import base64
571 | # import json
572 | # import time
573 | # import email
574 | # import asyncio
575 | # import logging
576 | # from aiohttp import web
577 | # import socket
578 | # from email.mime.text import MIMEText
579 | # from google.oauth2.credentials import Credentials
580 | # from google_auth_oauthlib.flow import InstalledAppFlow
581 | # from google.auth.transport.requests import Request
582 | # from googleapiclient.discovery import build
583 | 
584 | # # Configure logging
585 | # logging.basicConfig(
586 | #     level=logging.INFO,
587 | #     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
588 | # )
589 | # logger = logging.getLogger(__name__)
590 | 
591 | # # Gmail API configuration
592 | # SCOPES = ['https://www.googleapis.com/auth/gmail.readonly',
593 | #           'https://www.googleapis.com/auth/gmail.send']
594 | # TOKEN_FILE = 'token.json'
595 | # CREDENTIALS_FILE = 'credentials.json'
596 | 
597 | # # MCP Server configuration
598 | # MCP_HOST = '127.0.0.1'
599 | # MCP_PORT = 9999
600 | # WEB_PORT = 8080
601 | 
602 | # class GmailClient:
603 | #     def __init__(self):
604 | #         self.service = None
605 | #         self.authenticated = False
606 |     
607 | #     def authenticate(self):
608 | #         """Authenticate with Gmail API."""
609 | #         creds = None
610 | #         if os.path.exists(TOKEN_FILE):
611 | #             try:
612 | #                 creds = Credentials.from_authorized_user_info(
613 | #                     json.loads(open(TOKEN_FILE).read()), SCOPES)
614 | #             except Exception as e:
615 | #                 logger.error(f"Error loading credentials: {e}")
616 |         
617 | #         if not creds or not creds.valid:
618 | #             if creds and creds.expired and creds.refresh_token:
619 | #                 try:
620 | #                     creds.refresh(Request())
621 | #                 except Exception as e:
622 | #                     logger.error(f"Error refreshing credentials: {e}")
623 | #                     creds = None
624 |             
625 | #             if not creds:
626 | #                 if not os.path.exists(CREDENTIALS_FILE):
627 | #                     logger.error(f"Credentials file not found: {CREDENTIALS_FILE}")
628 | #                     return False
629 |                     
630 | #                 try:
631 | #                     flow = InstalledAppFlow.from_client_secrets_file(
632 | #                         CREDENTIALS_FILE, SCOPES)
633 | #                     creds = flow.run_local_server(port=0)
634 | #                 except Exception as e:
635 | #                     logger.error(f"Error in authentication flow: {e}")
636 | #                     return False
637 |                     
638 | #                 # Save the credentials for the next run
639 | #                 with open(TOKEN_FILE, 'w') as token:
640 | #                     token.write(creds.to_json())
641 |         
642 | #         try:
643 | #             self.service = build('gmail', 'v1', credentials=creds)
644 | #             self.authenticated = True
645 | #             logger.info("Successfully authenticated with Gmail API")
646 | #             return True
647 | #         except Exception as e:
648 | #             logger.error(f"Error building Gmail service: {e}")
649 | #             return False
650 |     
651 | #     async def get_messages(self, query="", max_results=10):
652 | #         """Get messages from Gmail."""
653 | #         if not self.authenticated:
654 | #             if not self.authenticate():
655 | #                 return []
656 |                 
657 | #         try:
658 | #             results = self.service.users().messages().list(
659 | #                 userId='me', q=query, maxResults=max_results).execute()
660 | #             messages = results.get('messages', [])
661 |             
662 | #             detailed_messages = []
663 | #             for msg in messages:
664 | #                 message = self.service.users().messages().get(
665 | #                     userId='me', id=msg['id']).execute()
666 |                 
667 | #                 headers = {}
668 | #                 for header in message['payload']['headers']:
669 | #                     headers[header['name']] = header['value']
670 |                 
671 | #                 subject = headers.get('Subject', '(No Subject)')
672 | #                 sender = headers.get('From', '(Unknown Sender)')
673 | #                 date = headers.get('Date', '')
674 |                 
675 | #                 body = ""
676 | #                 if 'parts' in message['payload']:
677 | #                     for part in message['payload']['parts']:
678 | #                         if part['mimeType'] == 'text/plain':
679 | #                             body = base64.urlsafe_b64decode(
680 | #                                 part['body']['data']).decode('utf-8')
681 | #                             break
682 | #                 elif 'body' in message['payload'] and 'data' in message['payload']['body']:
683 | #                     body = base64.urlsafe_b64decode(
684 | #                         message['payload']['body']['data']).decode('utf-8')
685 |                 
686 | #                 detailed_messages.append({
687 | #                     'id': msg['id'],
688 | #                     'threadId': message['threadId'],
689 | #                     'subject': subject,
690 | #                     'sender': sender,
691 | #                     'date': date,
692 | #                     'snippet': message['snippet'],
693 | #                     'body': body
694 | #                 })
695 |             
696 | #             return detailed_messages
697 | #         except Exception as e:
698 | #             logger.error(f"Error fetching messages: {e}")
699 | #             return []
700 |     
701 | #     async def send_message(self, to, subject, body):
702 | #         """Send a message through Gmail."""
703 | #         if not self.authenticated:
704 | #             if not self.authenticate():
705 | #                 return False
706 |         
707 | #         try:
708 | #             msg = MIMEText(body)
709 | #             msg['to'] = to
710 | #             msg['subject'] = subject
711 |             
712 | #             raw_message = base64.urlsafe_b64encode(msg.as_bytes()).decode('utf-8')
713 | #             message = self.service.users().messages().send(
714 | #                 userId='me', body={'raw': raw_message}).execute()
715 |             
716 | #             logger.info(f"Message sent. Message ID: {message['id']}")
717 | #             return True
718 | #         except Exception as e:
719 | #             logger.error(f"Error sending message: {e}")
720 | #             return False
721 | 
722 | # class MCPServer:
723 | #     def __init__(self, gmail_client):
724 | #         self.gmail_client = gmail_client
725 | #         self.clients = set()
726 | #         self.server = None
727 |     
728 | #     async def start_server(self):
729 | #         """Start the MCP server."""
730 | #         self.server = await asyncio.start_server(
731 | #             self.handle_client, MCP_HOST, MCP_PORT)
732 |         
733 | #         addr = self.server.sockets[0].getsockname()
734 | #         logger.info(f'MCP Server started on {addr}')
735 |         
736 | #         async with self.server:
737 | #             await self.server.serve_forever()
738 |     
739 | #     async def handle_client(self, reader, writer):
740 | #         """Handle MCP client connections."""
741 | #         addr = writer.get_extra_info('peername')
742 | #         logger.info(f'New client connection from {addr}')
743 | #         self.clients.add(writer)
744 |         
745 | #         try:
746 | #             while True:
747 | #                 data = await reader.readline()
748 | #                 if not data:
749 | #                     break
750 |                 
751 | #                 message = data.decode('utf-8').strip()
752 | #                 logger.info(f'Received: {message} from {addr}')
753 |                 
754 | #                 try:
755 | #                     cmd = json.loads(message)
756 | #                     response = await self.process_command(cmd)
757 | #                     writer.write(json.dumps(response).encode('utf-8') + b'\n')
758 | #                     await writer.drain()
759 | #                 except json.JSONDecodeError:
760 | #                     writer.write(b'{"status": "error", "message": "Invalid JSON"}\n')
761 | #                     await writer.drain()
762 | #         except Exception as e:
763 | #             logger.error(f"Error handling client: {e}")
764 | #         finally:
765 | #             writer.close()
766 | #             await writer.wait_closed()
767 | #             self.clients.remove(writer)
768 | #             logger.info(f'Connection closed for {addr}')
769 |     
770 | #     async def process_command(self, cmd):
771 | #         """Process MCP commands."""
772 | #         try:
773 | #             command = cmd.get('command', '')
774 |             
775 | #             if command == 'get_messages':
776 | #                 query = cmd.get('query', '')
777 | #                 max_results = cmd.get('max_results', 10)
778 | #                 messages = await self.gmail_client.get_messages(query, max_results)
779 | #                 return {'status': 'ok', 'messages': messages}
780 |             
781 | #             elif command == 'send_message':
782 | #                 to = cmd.get('to', '')
783 | #                 subject = cmd.get('subject', '')
784 | #                 body = cmd.get('body', '')
785 |                 
786 | #                 if not to or not subject or not body:
787 | #                     return {'status': 'error', 'message': 'Missing required fields'}
788 |                     
789 | #                 success = await self.gmail_client.send_message(to, subject, body)
790 | #                 if success:
791 | #                     return {'status': 'ok', 'message': 'Message sent successfully'}
792 | #                 else:
793 | #                     return {'status': 'error', 'message': 'Failed to send message'}
794 |             
795 | #             elif command == 'ping':
796 | #                 return {'status': 'ok', 'pong': time.time()}
797 |                 
798 | #             else:
799 | #                 return {'status': 'error', 'message': f'Unknown command: {command}'}
800 |         
801 | #         except Exception as e:
802 | #             logger.error(f"Error processing command: {e}")
803 | #             return {'status': 'error', 'message': str(e)}
804 | 
805 | # class WebServer:
806 | #     def __init__(self, gmail_client):
807 | #         self.gmail_client = gmail_client
808 | #         self.app = web.Application()
809 | #         self.setup_routes()
810 |     
811 | #     def setup_routes(self):
812 | #         """Set up web routes."""
813 | #         self.app.add_routes([
814 | #             web.get('/', self.handle_index),
815 | #             web.get('/api/messages', self.handle_get_messages),
816 | #             web.post('/api/send', self.handle_send_message),
817 | #             web.get('/auth', self.handle_auth),
818 | #         ])
819 |     
820 | #     async def handle_index(self, request):
821 | #         """Handle index route."""
822 | #         return web.Response(text="Gmail-MCP Server API", content_type='text/plain')
823 |     
824 | #     async def handle_get_messages(self, request):
825 | #         """Handle API request to get messages."""
826 | #         query = request.query.get('q', '')
827 | #         max_results = int(request.query.get('max', 10))
828 |         
829 | #         messages = await self.gmail_client.get_messages(query, max_results)
830 | #         return web.json_response({'status': 'ok', 'messages': messages})
831 |     
832 | #     async def handle_send_message(self, request):
833 | #         """Handle API request to send a message."""
834 | #         try:
835 | #             data = await request.json()
836 | #             to = data.get('to', '')
837 | #             subject = data.get('subject', '')
838 | #             body = data.get('body', '')
839 |             
840 | #             if not to or not subject or not body:
841 | #                 return web.json_response(
842 | #                     {'status': 'error', 'message': 'Missing required fields'}, 
843 | #                     status=400
844 | #                 )
845 |                 
846 | #             success = await self.gmail_client.send_message(to, subject, body)
847 | #             if success:
848 | #                 return web.json_response({'status': 'ok', 'message': 'Message sent successfully'})
849 | #             else:
850 | #                 return web.json_response(
851 | #                     {'status': 'error', 'message': 'Failed to send message'}, 
852 | #                     status=500
853 | #                 )
854 | #         except Exception as e:
855 | #             return web.json_response(
856 | #                 {'status': 'error', 'message': str(e)}, 
857 | #                 status=500
858 | #             )
859 |     
860 | #     async def handle_auth(self, request):
861 | #         """Handle authentication."""
862 | #         if self.gmail_client.authenticate():
863 | #             return web.Response(text="Authentication successful")
864 | #         else:
865 | #             return web.Response(text="Authentication failed", status=500)
866 |     
867 | #     async def start_server(self):
868 | #         """Start the web server."""
869 | #         runner = web.AppRunner(self.app)
870 | #         await runner.setup()
871 | #         site = web.TCPSite(runner, 'localhost', WEB_PORT)
872 | #         await site.start()
873 | #         logger.info(f'Web server started on http://localhost:{WEB_PORT}')
874 | 
875 | # async def main():
876 | #     """Main entry point."""
877 | #     gmail_client = GmailClient()
878 |     
879 | #     # Initialize the MCP server
880 | #     mcp_server = MCPServer(gmail_client)
881 |     
882 | #     # Initialize the web server
883 | #     web_server = WebServer(gmail_client)
884 |     
885 | #     # Run both servers concurrently
886 | #     await asyncio.gather(
887 | #         mcp_server.start_server(),
888 | #         web_server.start_server()
889 | #     )
890 | 
891 | # if __name__ == '__main__':
892 | #     try:
893 | #         asyncio.run(main())
894 | #     except KeyboardInterrupt:
895 | #         logger.info("Server shutdown by user")
```