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

```
├── .gitignore
├── .python-version
├── images
│   └── demo.mov
├── LICENSE
├── pyproject.toml
├── README.md
├── src
│   └── gmail
│       ├── __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 | *.DS_Store
 9 | 
10 | # Virtual environments
11 | .venv
12 | 
```

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

```markdown
  1 | # Gmail Server for Model Context Protocol (MCP)
  2 | 
  3 | This MCP server integrates with Gmail to enable sending, removing, reading, drafting, and responding to emails.
  4 | 
  5 | > Note: This server enables an MCP client to read, remove, and send emails. However, the client prompts the user before conducting such activities. 
  6 | 
  7 | https://github.com/user-attachments/assets/5794cd16-00d2-45a2-884a-8ba0c3a90c90
  8 | 
  9 | 
 10 | ## Components
 11 | 
 12 | ### Tools
 13 | 
 14 | - **send-email**
 15 |   - Sends email to email address recipient 
 16 |   - Input:
 17 |     - `recipient_id` (string): Email address of addressee
 18 |     - `subject` (string): Email subject
 19 |     - `message` (string): Email content
 20 |   - Returns status and message_id
 21 | 
 22 | - **trash-email**
 23 |   - Moves email to trash 
 24 |   - Input:
 25 |     - `email_id` (string): Auto-generated ID of email
 26 |   - Returns success message
 27 | 
 28 | - **mark-email-as-read**
 29 |   - Marks email as read 
 30 |   - Input:
 31 |     - `email_id` (string): Auto-generated ID of email
 32 |   - Returns success message
 33 | 
 34 | - **get-unread-emails**
 35 |   - Retrieves unread emails 
 36 |   - Returns list of emails including email ID
 37 | 
 38 | - **read-email**
 39 |   - Retrieves given email content
 40 |   - Input:
 41 |     - `email_id` (string): Auto-generated ID of email
 42 |   - Returns dictionary of email metadata and marks email as read
 43 | 
 44 | - **open-email**
 45 |   - Open email in browser
 46 |   - Input:
 47 |     - `email_id` (string): Auto-generated ID of email
 48 |   - Returns success message and opens given email in default browser
 49 | 
 50 | 
 51 | ## Setup
 52 | 
 53 | ### Gmail API Setup
 54 | 
 55 | 1. [Create a new Google Cloud project](https://console.cloud.google.com/projectcreate)
 56 | 2. [Enable the Gmail API](https://console.cloud.google.com/workspace-api/products)
 57 | 3. [Configure an OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) 
 58 |     - Select "external". However, we will not publish the app.
 59 |     - Add your personal email address as a "Test user".
 60 | 4. Add OAuth scope `https://www.googleapis.com/auth/gmail/modify`
 61 | 5. [Create an OAuth Client ID](https://console.cloud.google.com/apis/credentials/oauthclient) for application type "Desktop App"
 62 | 6. Download the JSON file of your client's OAuth keys
 63 | 7. Rename the key file and save it to your local machine in a secure location. Take note of the location.
 64 |     - The absolute path to this file will be passed as parameter `--creds-file-path` when the server is started. 
 65 | 
 66 | ### Authentication
 67 | 
 68 | When the server is started, an authentication flow will be launched in your system browser. 
 69 | Token credentials will be subsequently saved (and later retrieved) in the absolute file path passed to parameter `--token-path`.
 70 | 
 71 | For example, you may use a dot directory in your home folder, replacing `[your-home-folder]`.:
 72 | 
 73 | | Parameter       | Example                                          |
 74 | |-----------------|--------------------------------------------------|
 75 | | `--creds-file-path` | `/[your-home-folder]/.google/client_creds.json` |
 76 | | `--token-path`      | `/[your-home-folder]/.google/app_tokens.json`    |
 77 | 
 78 | 
 79 | ### Usage with Desktop App
 80 | 
 81 | Using [uv](https://docs.astral.sh/uv/) is recommended.
 82 | 
 83 | To integrate this server with Claude Desktop as the MCP Client, add the following to your app's server configuration. By default, this is stored as `~/Library/Application\ Support/Claude/claude_desktop_config.json`. 
 84 | 
 85 | ```json
 86 | {
 87 |   "mcpServers": {
 88 |     "gdrive": {
 89 |       "command": "uv",
 90 |       "args": [
 91 |         "--directory",
 92 |         "[absolute-path-to-git-repo]",
 93 |         "run",
 94 |         "gmail",
 95 |         "--creds-file-path",
 96 |         "[absolute-path-to-credentials-file]",
 97 |         "--token-path",
 98 |         "[absolute-path-to-access-tokens-file]"
 99 |       ]
100 |     }
101 |   }
102 | }
103 | ```
104 | 
105 | The following parameters must be set
106 | | Parameter       | Example                                          |
107 | |-----------------|--------------------------------------------------|
108 | | `--directory`   | Absolute path to `gmail` directory containing server |
109 | | `--creds-file-path` | Absolute path to credentials file created in Gmail API Setup. |
110 | | `--token-path`      | Absolute path to store and retrieve access and refresh tokens for application.  |
111 | 
112 | ### Troubleshooting with MCP Inspector
113 | 
114 | To test the server, use [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector).
115 | From the git repo, run the below changing the parameter arguments accordingly.
116 | 
117 | ```bash
118 | npx @modelcontextprotocol/inspector uv run [absolute-path-to-git-repo]/src/gmail/server.py --creds-file-path [absolute-path-to-credentials-file] --token-path [absolute-path-to-access-tokens-file]
119 | ```
120 | 
121 | 
```

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

```toml
 1 | [project]
 2 | name = "gmail"
 3 | version = "0.1.0"
 4 | description = "Model Context Protocol server for gmail"
 5 | readme = "README.md"
 6 | requires-python = ">=3.12"
 7 | dependencies = [
 8 |     "httpx>=0.28.1",
 9 |     "mcp>=1.1.2",
10 |     "google-api-python-client>=2.156.0",
11 |     "google-auth-httplib2>=0.2.0",
12 |     "google-auth-oauthlib>=1.2.1",
13 | ]
14 | [build-system]
15 | requires = [ "hatchling",]
16 | build-backend = "hatchling.build"
17 | 
18 | [project.scripts]
19 | gmail = "gmail:main"
20 | 
```

--------------------------------------------------------------------------------
/src/gmail/__init__.py:
--------------------------------------------------------------------------------

```python
 1 | from . import server
 2 | import asyncio
 3 | import argparse
 4 | 
 5 | def main():
 6 |     """Main entry point for the package."""
 7 |     parser = argparse.ArgumentParser(description='Gmail API MCP Server')
 8 |     parser.add_argument('--creds-file-path',
 9 |                         required=True,
10 |                        help='OAuth 2.0 credentials file path')
11 |     parser.add_argument('--token-path',
12 |                         required=True,
13 |                        help='File location to store and retrieve access and refresh tokens for application')
14 |     
15 |     args = parser.parse_args()
16 |     asyncio.run(server.main(args.creds_file_path, args.token_path))
17 | 
18 | # Optionally expose other important items at package level
19 | __all__ = ['main', 'server']
20 | 
```

--------------------------------------------------------------------------------
/src/gmail/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[str, str]]| str:
191 |         """
192 |         Retrieves unread messages from mailbox.
193 |         Returns list of messsage IDs in key 'id'."""
194 |         try:
195 |             user_id = 'me'
196 |             query = 'in:inbox is:unread category:primary'
197 | 
198 |             response = self.service.users().messages().list(userId=user_id,
199 |                                                         q=query).execute()
200 |             messages = []
201 |             if 'messages' in response:
202 |                 messages.extend(response['messages'])
203 | 
204 |             while 'nextPageToken' in response:
205 |                 page_token = response['nextPageToken']
206 |                 response = self.service.users().messages().list(userId=user_id, q=query,
207 |                                                     pageToken=page_token).execute()
208 |                 messages.extend(response['messages'])
209 |             return messages
210 | 
211 |         except HttpError as error:
212 |             return f"An HttpError occurred: {str(error)}"
213 | 
214 |     async def read_email(self, email_id: str) -> dict[str, str]| str:
215 |         """Retrieves email contents including to, from, subject, and contents."""
216 |         try:
217 |             msg = self.service.users().messages().get(userId="me", id=email_id, format='raw').execute()
218 |             email_metadata = {}
219 | 
220 |             # Decode the base64URL encoded raw content
221 |             raw_data = msg['raw']
222 |             decoded_data = urlsafe_b64decode(raw_data)
223 | 
224 |             # Parse the RFC 2822 email
225 |             mime_message = message_from_bytes(decoded_data)
226 | 
227 |             # Extract the email body
228 |             body = None
229 |             if mime_message.is_multipart():
230 |                 for part in mime_message.walk():
231 |                     # Extract the text/plain part
232 |                     if part.get_content_type() == "text/plain":
233 |                         body = part.get_payload(decode=True).decode()
234 |                         break
235 |             else:
236 |                 # For non-multipart messages
237 |                 body = mime_message.get_payload(decode=True).decode()
238 |             email_metadata['content'] = body
239 |             
240 |             # Extract metadata
241 |             email_metadata['subject'] = decode_mime_header(mime_message.get('subject', ''))
242 |             email_metadata['from'] = mime_message.get('from','')
243 |             email_metadata['to'] = mime_message.get('to','')
244 |             email_metadata['date'] = mime_message.get('date','')
245 |             
246 |             logger.info(f"Email read: {email_id}")
247 |             
248 |             # We want to mark email as read once we read it
249 |             await self.mark_email_as_read(email_id)
250 | 
251 |             return email_metadata
252 |         except HttpError as error:
253 |             return f"An HttpError occurred: {str(error)}"
254 |         
255 |     async def trash_email(self, email_id: str) -> str:
256 |         """Moves email to trash given ID."""
257 |         try:
258 |             self.service.users().messages().trash(userId="me", id=email_id).execute()
259 |             logger.info(f"Email moved to trash: {email_id}")
260 |             return "Email moved to trash successfully."
261 |         except HttpError as error:
262 |             return f"An HttpError occurred: {str(error)}"
263 |         
264 |     async def mark_email_as_read(self, email_id: str) -> str:
265 |         """Marks email as read given ID."""
266 |         try:
267 |             self.service.users().messages().modify(userId="me", id=email_id, body={'removeLabelIds': ['UNREAD']}).execute()
268 |             logger.info(f"Email marked as read: {email_id}")
269 |             return "Email marked as read."
270 |         except HttpError as error:
271 |             return f"An HttpError occurred: {str(error)}"
272 |   
273 | async def main(creds_file_path: str,
274 |                token_path: str):
275 |     
276 |     gmail_service = GmailService(creds_file_path, token_path)
277 |     server = Server("gmail")
278 | 
279 |     @server.list_prompts()
280 |     async def list_prompts() -> list[types.Prompt]:
281 |         return list(PROMPTS.values())
282 | 
283 |     @server.get_prompt()
284 |     async def get_prompt(
285 |         name: str, arguments: dict[str, str] | None = None
286 |     ) -> types.GetPromptResult:
287 |         if name not in PROMPTS:
288 |             raise ValueError(f"Prompt not found: {name}")
289 | 
290 |         if name == "manage-email":
291 |             return types.GetPromptResult(
292 |                 messages=[
293 |                     types.PromptMessage(
294 |                         role="user",
295 |                         content=types.TextContent(
296 |                             type="text",
297 |                             text=EMAIL_ADMIN_PROMPTS,
298 |                         )
299 |                     )
300 |                 ]
301 |             )
302 | 
303 |         if name == "draft-email":
304 |             content = arguments.get("content", "")
305 |             recipient = arguments.get("recipient", "")
306 |             recipient_email = arguments.get("recipient_email", "")
307 |             
308 |             # First message asks the LLM to create the draft
309 |             return types.GetPromptResult(
310 |                 messages=[
311 |                     types.PromptMessage(
312 |                         role="user",
313 |                         content=types.TextContent(
314 |                             type="text",
315 |                             text=f"""Please draft an email about {content} for {recipient} ({recipient_email}).
316 |                             Include a subject line starting with 'Subject:' on the first line.
317 |                             Do not send the email yet, just draft it and ask the user for their thoughts."""
318 |                         )
319 |                     )
320 |                 ]
321 |             )
322 |         
323 |         elif name == "edit-draft":
324 |             changes = arguments.get("changes", "")
325 |             current_draft = arguments.get("current_draft", "")
326 |             
327 |             # Edit existing draft based on requested changes
328 |             return types.GetPromptResult(
329 |                 messages=[
330 |                     types.PromptMessage(
331 |                         role="user",
332 |                         content=types.TextContent(
333 |                             type="text",
334 |                             text=f"""Please revise the current email draft:
335 |                             {current_draft}
336 |                             
337 |                             Requested changes:
338 |                             {changes}
339 |                             
340 |                             Please provide the updated draft."""
341 |                         )
342 |                     )
343 |                 ]
344 |             )
345 | 
346 |         raise ValueError("Prompt implementation not found")
347 | 
348 |     @server.list_tools()
349 |     async def handle_list_tools() -> list[types.Tool]:
350 |         return [
351 |             types.Tool(
352 |                 name="send-email",
353 |                 description="""Sends email to recipient. 
354 |                 Do not use if user only asked to draft email. 
355 |                 Drafts must be approved before sending.""",
356 |                 inputSchema={
357 |                     "type": "object",
358 |                     "properties": {
359 |                         "recipient_id": {
360 |                             "type": "string",
361 |                             "description": "Recipient email address",
362 |                         },
363 |                         "subject": {
364 |                             "type": "string",
365 |                             "description": "Email subject",
366 |                         },
367 |                         "message": {
368 |                             "type": "string",
369 |                             "description": "Email content text",
370 |                         },
371 |                     },
372 |                     "required": ["recipient_id", "subject", "message"],
373 |                 },
374 |             ),
375 |             types.Tool(
376 |                 name="trash-email",
377 |                 description="""Moves email to trash. 
378 |                 Confirm before moving email to trash.""",
379 |                 inputSchema={
380 |                     "type": "object",
381 |                     "properties": {
382 |                         "email_id": {
383 |                             "type": "string",
384 |                             "description": "Email ID",
385 |                         },
386 |                     },
387 |                     "required": ["email_id"],
388 |                 },
389 |             ),
390 |             types.Tool(
391 |                 name="get-unread-emails",
392 |                 description="Retrieve unread emails",
393 |                 inputSchema={
394 |                     "type": "object",
395 |                     "properties": {"":""},
396 |                     "required": None
397 |                 },
398 |             ),
399 |             types.Tool(
400 |                 name="read-email",
401 |                 description="Retrieves given email content",
402 |                 inputSchema={
403 |                     "type": "object",
404 |                     "properties": {
405 |                         "email_id": {
406 |                             "type": "string",
407 |                             "description": "Email ID",
408 |                         },
409 |                     },
410 |                     "required": ["email_id"],
411 |                 },
412 |             ),
413 |             types.Tool(
414 |                 name="mark-email-as-read",
415 |                 description="Marks given email as read",
416 |                 inputSchema={
417 |                     "type": "object",
418 |                     "properties": {
419 |                         "email_id": {
420 |                             "type": "string",
421 |                             "description": "Email ID",
422 |                         },
423 |                     },
424 |                     "required": ["email_id"],
425 |                 },
426 |             ),
427 |             types.Tool(
428 |                 name="open-email",
429 |                 description="Open email in browser",
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 |         ]
442 | 
443 |     @server.call_tool()
444 |     async def handle_call_tool(
445 |         name: str, arguments: dict | None
446 |     ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
447 | 
448 |         if name == "send-email":
449 |             recipient = arguments.get("recipient_id")
450 |             if not recipient:
451 |                 raise ValueError("Missing recipient parameter")
452 |             subject = arguments.get("subject")
453 |             if not subject:
454 |                 raise ValueError("Missing subject parameter")
455 |             message = arguments.get("message")
456 |             if not message:
457 |                 raise ValueError("Missing message parameter")
458 |                 
459 |             # Extract subject and message content
460 |             email_lines = message.split('\n')
461 |             if email_lines[0].startswith('Subject:'):
462 |                 subject = email_lines[0][8:].strip()
463 |                 message_content = '\n'.join(email_lines[1:]).strip()
464 |             else:
465 |                 message_content = message
466 |                 
467 |             send_response = await gmail_service.send_email(recipient, subject, message_content)
468 |             
469 |             if send_response["status"] == "success":
470 |                 response_text = f"Email sent successfully. Message ID: {send_response['message_id']}"
471 |             else:
472 |                 response_text = f"Failed to send email: {send_response['error_message']}"
473 |             return [types.TextContent(type="text", text=response_text)]
474 | 
475 |         if name == "get-unread-emails":
476 |                 
477 |             unread_emails = await gmail_service.get_unread_emails()
478 |             return [types.TextContent(type="text", text=str(unread_emails),artifact={"type": "json", "data": unread_emails} )]
479 |         
480 |         if name == "read-email":
481 |             email_id = arguments.get("email_id")
482 |             if not email_id:
483 |                 raise ValueError("Missing email ID parameter")
484 |                 
485 |             retrieved_email = await gmail_service.read_email(email_id)
486 |             return [types.TextContent(type="text", text=str(retrieved_email),artifact={"type": "dictionary", "data": retrieved_email} )]
487 |         if name == "open-email":
488 |             email_id = arguments.get("email_id")
489 |             if not email_id:
490 |                 raise ValueError("Missing email ID parameter")
491 |                 
492 |             msg = await gmail_service.open_email(email_id)
493 |             return [types.TextContent(type="text", text=str(msg))]
494 |         if name == "trash-email":
495 |             email_id = arguments.get("email_id")
496 |             if not email_id:
497 |                 raise ValueError("Missing email ID parameter")
498 |                 
499 |             msg = await gmail_service.trash_email(email_id)
500 |             return [types.TextContent(type="text", text=str(msg))]
501 |         if name == "mark-email-as-read":
502 |             email_id = arguments.get("email_id")
503 |             if not email_id:
504 |                 raise ValueError("Missing email ID parameter")
505 |                 
506 |             msg = await gmail_service.mark_email_as_read(email_id)
507 |             return [types.TextContent(type="text", text=str(msg))]
508 |         else:
509 |             logger.error(f"Unknown tool: {name}")
510 |             raise ValueError(f"Unknown tool: {name}")
511 | 
512 |     async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
513 |         await server.run(
514 |             read_stream,
515 |             write_stream,
516 |             InitializationOptions(
517 |                 server_name="gmail",
518 |                 server_version="0.1.0",
519 |                 capabilities=server.get_capabilities(
520 |                     notification_options=NotificationOptions(),
521 |                     experimental_capabilities={},
522 |                 ),
523 |             ),
524 |         )
525 | 
526 | if __name__ == "__main__":
527 |     parser = argparse.ArgumentParser(description='Gmail API MCP Server')
528 |     parser.add_argument('--creds-file-path',
529 |                         required=True,
530 |                        help='OAuth 2.0 credentials file path')
531 |     parser.add_argument('--token-path',
532 |                         required=True,
533 |                        help='File location to store and retrieve access and refresh tokens for application')
534 |     
535 |     args = parser.parse_args()
536 |     asyncio.run(main(args.creds_file_path, args.token_path))
```