google-workspace-mcp 1.0.0__py3-none-any.whl

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.
Files changed (38) hide show
  1. google_workspace_mcp/__init__.py +3 -0
  2. google_workspace_mcp/__main__.py +43 -0
  3. google_workspace_mcp/app.py +8 -0
  4. google_workspace_mcp/auth/__init__.py +7 -0
  5. google_workspace_mcp/auth/gauth.py +62 -0
  6. google_workspace_mcp/config.py +60 -0
  7. google_workspace_mcp/prompts/__init__.py +3 -0
  8. google_workspace_mcp/prompts/calendar.py +36 -0
  9. google_workspace_mcp/prompts/drive.py +18 -0
  10. google_workspace_mcp/prompts/gmail.py +65 -0
  11. google_workspace_mcp/prompts/slides.py +40 -0
  12. google_workspace_mcp/resources/__init__.py +13 -0
  13. google_workspace_mcp/resources/calendar.py +79 -0
  14. google_workspace_mcp/resources/drive.py +93 -0
  15. google_workspace_mcp/resources/gmail.py +58 -0
  16. google_workspace_mcp/resources/sheets_resources.py +92 -0
  17. google_workspace_mcp/resources/slides.py +421 -0
  18. google_workspace_mcp/services/__init__.py +21 -0
  19. google_workspace_mcp/services/base.py +73 -0
  20. google_workspace_mcp/services/calendar.py +256 -0
  21. google_workspace_mcp/services/docs_service.py +388 -0
  22. google_workspace_mcp/services/drive.py +454 -0
  23. google_workspace_mcp/services/gmail.py +676 -0
  24. google_workspace_mcp/services/sheets_service.py +466 -0
  25. google_workspace_mcp/services/slides.py +959 -0
  26. google_workspace_mcp/tools/__init__.py +7 -0
  27. google_workspace_mcp/tools/calendar.py +229 -0
  28. google_workspace_mcp/tools/docs_tools.py +277 -0
  29. google_workspace_mcp/tools/drive.py +221 -0
  30. google_workspace_mcp/tools/gmail.py +344 -0
  31. google_workspace_mcp/tools/sheets_tools.py +322 -0
  32. google_workspace_mcp/tools/slides.py +478 -0
  33. google_workspace_mcp/utils/__init__.py +1 -0
  34. google_workspace_mcp/utils/markdown_slides.py +504 -0
  35. google_workspace_mcp-1.0.0.dist-info/METADATA +547 -0
  36. google_workspace_mcp-1.0.0.dist-info/RECORD +38 -0
  37. google_workspace_mcp-1.0.0.dist-info/WHEEL +4 -0
  38. google_workspace_mcp-1.0.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,3 @@
1
+ """Google Workspace MCP - Google Workspace integration for Model Context Protocol."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,43 @@
1
+ """
2
+ Main entry point for running the MCP server via python -m google_workspace_mcp
3
+ """
4
+
5
+ import asyncio
6
+
7
+ from google_workspace_mcp import config # noqa: F401
8
+ from google_workspace_mcp.app import mcp # Import instance from central location
9
+
10
+ # Import all modules that register components with the FastMCP instance
11
+ from google_workspace_mcp.prompts import calendar as calendar_prompts # noqa: F401
12
+ from google_workspace_mcp.prompts import drive as drive_prompts # noqa: F401
13
+ from google_workspace_mcp.prompts import gmail as gmail_prompts # noqa: F401
14
+ from google_workspace_mcp.prompts import slides as slides_prompts # noqa: F401
15
+
16
+ # Register resources
17
+ from google_workspace_mcp.resources import calendar as calendar_resources # noqa: F401
18
+ from google_workspace_mcp.resources import drive as drive_resources # noqa: F401
19
+ from google_workspace_mcp.resources import gmail as gmail_resources # noqa: F401
20
+ from google_workspace_mcp.resources import sheets_resources # noqa: F401
21
+ from google_workspace_mcp.resources import slides as slides_resources # noqa: F401
22
+
23
+ # Register tools
24
+ from google_workspace_mcp.tools import ( # noqa: F401
25
+ calendar_tools,
26
+ docs_tools,
27
+ drive_tools,
28
+ gmail_tools,
29
+ sheets_tools,
30
+ slides_tools,
31
+ )
32
+
33
+
34
+ def main():
35
+ """Main entry point for the MCP server."""
36
+ try:
37
+ asyncio.run(mcp.run())
38
+ except KeyboardInterrupt:
39
+ print("\nShutting down MCP server...")
40
+
41
+
42
+ if __name__ == "__main__":
43
+ main()
@@ -0,0 +1,8 @@
1
+ """
2
+ Central application instance definition.
3
+ """
4
+
5
+ from mcp.server.fastmcp import FastMCP
6
+
7
+ # Central FastMCP instance
8
+ mcp = FastMCP(name="google-workspace-mcp")
@@ -0,0 +1,7 @@
1
+ """
2
+ Authentication utilities for Google Workspace MCP.
3
+ """
4
+
5
+ from .gauth import get_credentials
6
+
7
+ __all__ = ["get_credentials"]
@@ -0,0 +1,62 @@
1
+ """
2
+ Google authentication utilities for Google Workspace MCP.
3
+
4
+ Handles OAuth 2.0 authentication using environment variables.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ from functools import lru_cache
10
+
11
+ import google.auth.exceptions
12
+ import google.auth.transport.requests
13
+ from google.oauth2.credentials import Credentials
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @lru_cache(maxsize=32)
19
+ def get_credentials():
20
+ """
21
+ Retrieves Google OAuth credentials from environment variables.
22
+
23
+ The credentials are cached to avoid redundant processing, but will
24
+ be automatically refreshed when expired.
25
+
26
+ Returns:
27
+ google.oauth2.credentials.Credentials: Initialized credentials object.
28
+
29
+ Raises:
30
+ ValueError: If any of the required environment variables are missing.
31
+ CredentialsError: If credentials are invalid or revoked.
32
+ """
33
+ client_id = os.environ.get("GOOGLE_WORKSPACE_CLIENT_ID")
34
+ client_secret = os.environ.get("GOOGLE_WORKSPACE_CLIENT_SECRET")
35
+ refresh_token = os.environ.get("GOOGLE_WORKSPACE_REFRESH_TOKEN")
36
+
37
+ if not client_id:
38
+ raise ValueError("Environment variable 'GOOGLE_WORKSPACE_CLIENT_ID' is required but not set.")
39
+ if not client_secret:
40
+ raise ValueError("Environment variable 'GOOGLE_WORKSPACE_CLIENT_SECRET' is required but not set.")
41
+ if not refresh_token:
42
+ raise ValueError("Environment variable 'GOOGLE_WORKSPACE_REFRESH_TOKEN' is required but not set.")
43
+
44
+ logger.info("Successfully retrieved Google Workspace credentials from environment variables.")
45
+
46
+ try:
47
+ return Credentials(
48
+ token=None, # Will be fetched automatically on first use
49
+ refresh_token=refresh_token,
50
+ token_uri="https://oauth2.googleapis.com/token",
51
+ client_id=client_id,
52
+ client_secret=client_secret,
53
+ )
54
+ # The google-auth library automatically handles token refreshing
55
+ # when needed during API requests using the provided refresh token.
56
+
57
+ except google.auth.exceptions.RefreshError as e:
58
+ logger.exception("Credentials refresh failed - token may be revoked or expired")
59
+ raise ValueError(f"Invalid or revoked credentials: {e}") from e
60
+ except Exception as e:
61
+ logger.exception("Failed to create Credentials object")
62
+ raise ValueError(f"Failed to initialize credentials: {e}") from e
@@ -0,0 +1,60 @@
1
+ """
2
+ MCP server utilities for Google Workspace integration.
3
+ This file now contains utility functions, like parsing capabilities.
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ import os
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ # Parse enabled capabilities from environment
14
+ def get_enabled_capabilities() -> set[str]:
15
+ """
16
+ Get the set of enabled capabilities from environment variables.
17
+ Expects GOOGLE_WORKSPACE_ENABLED_CAPABILITIES to be a JSON array of strings.
18
+ E.g., '["drive", "gmail", "docs"]'
19
+
20
+ Returns:
21
+ set[str]: Enabled capability names
22
+ """
23
+ capabilities_str = os.environ.get("GOOGLE_WORKSPACE_ENABLED_CAPABILITIES", "[]") # Default to empty JSON list string
24
+
25
+ if not capabilities_str.strip():
26
+ capabilities_str = "[]" # Treat fully empty or whitespace-only string as empty list
27
+
28
+ capabilities: set[str] = set()
29
+ try:
30
+ parsed_list = json.loads(capabilities_str)
31
+ if isinstance(parsed_list, list) and all(isinstance(item, str) for item in parsed_list):
32
+ capabilities = {cap.strip().lower() for cap in parsed_list if cap.strip()}
33
+ else:
34
+ logger.warning(
35
+ f"GOOGLE_WORKSPACE_ENABLED_CAPABILITIES is not a valid JSON list of strings: {capabilities_str}. "
36
+ f"Found type: {type(parsed_list)}. All tools may be affected. "
37
+ 'Please use format like \'["drive", "gmail"]\'.'
38
+ )
39
+ # Fallback to empty set, effectively disabling tools unless Hub ignores this
40
+ # Or, decide if we should raise an error or parse common old format as fallback
41
+ # For now, strict parsing and warning.
42
+ except json.JSONDecodeError:
43
+ logger.warning(
44
+ f"GOOGLE_WORKSPACE_ENABLED_CAPABILITIES is not valid JSON: {capabilities_str}. "
45
+ 'All tools may be affected. Please use format like \'["drive", "gmail"]\'.'
46
+ )
47
+ # Fallback for non-JSON or badly formatted JSON
48
+ # Consider if a fallback to comma-separated parsing is desired for backward compatibility or remove.
49
+ # For now, strict parsing and warning.
50
+
51
+ if not capabilities: # This condition includes empty JSON array "[]"
52
+ logger.warning(
53
+ "No GOOGLE_WORKSPACE_ENABLED_CAPABILITIES specified or list is empty. "
54
+ "All tools might be disabled depending on Hub filtering. "
55
+ "(Note: FastMCP relies on Hub filtering based on declared capabilities.)"
56
+ )
57
+ else:
58
+ logger.info(f"Declared Google Workspace capabilities via env var: {capabilities}")
59
+
60
+ return capabilities
@@ -0,0 +1,3 @@
1
+ """MCP Prompt modules for Google Workspace."""
2
+
3
+ __all__ = []
@@ -0,0 +1,36 @@
1
+ """
2
+ Calendar prompts for Google Calendar operations.
3
+ """
4
+
5
+ import logging
6
+
7
+ from mcp.server.fastmcp.prompts.base import UserMessage
8
+ from mcp.server.fastmcp.server import Context
9
+
10
+ from google_workspace_mcp.app import mcp
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @mcp.prompt()
16
+ async def draft_calendar_agenda(
17
+ event_id: str,
18
+ calendar_id: str = "primary",
19
+ ctx: Context = None, # Add default None for ctx
20
+ ) -> list[UserMessage]:
21
+ """Drafts a meeting agenda based on event info (currently simulated)."""
22
+ logger.info(f"Executing draft_calendar_agenda prompt for event '{event_id}' on calendar '{calendar_id}'")
23
+ # TODO: Replace simulation with actual call to get event details via resource
24
+ # try:
25
+ # event_details_dict = await ctx.read_resource(f"calendar://{calendar_id}/event/{event_id}")
26
+ # event_details = f"Meeting: {event_details_dict.get('summary', 'No Title')}\n"
27
+ # event_details += f"Time: {event_details_dict.get('start',{}).get('dateTime')} - {event_details_dict.get('end',{}).get('dateTime')}\n"
28
+ # event_details += f"Description: {event_details_dict.get('description', 'N/A')}"
29
+ # except Exception as e:
30
+ # logger.warning(f"Could not get event details for {event_id}: {e}")
31
+ # event_details = f"Meeting: {event_id} on Calendar: {calendar_id} - Details unavailable."
32
+
33
+ # Simulate details for now
34
+ event_details = f"Meeting: {event_id} on Calendar: {calendar_id} - Details unavailable."
35
+
36
+ return [UserMessage(f"Please draft a simple meeting agenda based on the following event information:\n\n{event_details}")]
@@ -0,0 +1,18 @@
1
+ """Drive related MCP Prompts."""
2
+
3
+ import logging
4
+
5
+ from mcp.server.fastmcp.prompts.base import UserMessage
6
+
7
+ from google_workspace_mcp.app import mcp
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @mcp.prompt()
13
+ async def suggest_drive_outline(topic: str) -> list[UserMessage]:
14
+ """Suggests a document outline for a given topic."""
15
+ logger.info(f"Executing suggest_drive_outline prompt for topic: {topic}")
16
+ return [
17
+ UserMessage(f"Please suggest a standard document outline (sections and subsections) for a document about: {topic}")
18
+ ]
@@ -0,0 +1,65 @@
1
+ """
2
+ Gmail prompts for Gmail operations.
3
+ """
4
+
5
+ import logging
6
+
7
+ # Use specific types for clarity
8
+ from mcp.server.fastmcp.prompts.base import UserMessage
9
+ from mcp.server.fastmcp.server import Context
10
+
11
+ from google_workspace_mcp.app import mcp
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @mcp.prompt()
17
+ async def summarize_recent_emails(query: str, max_emails: int = 5, ctx: Context = None) -> list[UserMessage]:
18
+ """Summarizes recent emails based on a query."""
19
+ if ctx is None:
20
+ # This should ideally not happen if context injection works,
21
+ # but handle defensively for direct calls or tests.
22
+ logger.error("Context (ctx) is required for summarize_recent_emails but was not provided.")
23
+ # Return an error message or raise an exception?
24
+ # Raising seems more appropriate for a required dependency.
25
+ raise ValueError("Context (ctx) is required for this prompt.")
26
+
27
+ logger.info(f"Executing summarize_recent_emails prompt for query: '{query}', max: {max_emails}")
28
+
29
+ email_context = "No emails found or error fetching emails."
30
+ try:
31
+ # Construct resource URI - ensure query is URL-encoded if needed by framework
32
+ # Assuming FastMCP handles basic encoding or query is simple enough
33
+ resource_uri = f"gmail://search?q={query}"
34
+ logger.debug(f"Reading resource: {resource_uri}")
35
+
36
+ # Call the resource via context
37
+ email_data = await ctx.read_resource(resource_uri)
38
+ logger.debug(f"Resource returned: {email_data}")
39
+
40
+ if email_data and isinstance(email_data, dict) and "emails" in email_data:
41
+ emails = email_data["emails"]
42
+ if emails:
43
+ summary_parts = []
44
+ for _i, email in enumerate(emails[:max_emails]): # Limit results
45
+ subject = email.get("subject", "No Subject")
46
+ sender = email.get("from", "Unknown Sender")
47
+ snippet = email.get("snippet", "...")
48
+ summary_parts.append(f"- From: {sender}\n Subject: {subject}\n Snippet: {snippet}")
49
+
50
+ if summary_parts:
51
+ email_context = "\n".join(summary_parts)
52
+ else:
53
+ email_context = "No emails found matching the query."
54
+ elif email_data and isinstance(email_data, dict) and "message" in email_data:
55
+ # Handle case where resource returns a message like "No emails found"
56
+ email_context = email_data["message"]
57
+
58
+ except ValueError as ve:
59
+ logger.error(f"ValueError calling resource for email summary: {ve}")
60
+ email_context = f"Error: Could not fetch emails - {ve}"
61
+ except Exception as e:
62
+ logger.exception(f"Unexpected error calling resource for email summary: {e}")
63
+ email_context = "Error: An unexpected error occurred while fetching emails."
64
+
65
+ return [UserMessage(f"Please summarize the key points from these recent emails:\n\n{email_context}")]
@@ -0,0 +1,40 @@
1
+ """
2
+ Slides prompts for Google Slides operations.
3
+ """
4
+
5
+ import logging
6
+
7
+ from mcp.server.fastmcp.prompts.base import UserMessage
8
+ from mcp.server.fastmcp.server import Context
9
+
10
+ from google_workspace_mcp.app import mcp
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @mcp.prompt()
16
+ async def suggest_slide_content(presentation_topic: str, slide_objective: str) -> list[UserMessage]:
17
+ """Suggests content for a presentation slide."""
18
+ logger.info(f"Executing suggest_slide_content prompt for topic '{presentation_topic}' and objective '{slide_objective}'")
19
+ return [
20
+ UserMessage(
21
+ f"Generate content suggestions for one presentation slide.\n"
22
+ f"Topic: {presentation_topic}\n"
23
+ f"Objective: {slide_objective}\n"
24
+ f"Please provide a concise Title and 3-4 bullet points."
25
+ )
26
+ ]
27
+
28
+
29
+ @mcp.prompt()
30
+ def create_slides_presentation(title: str, ctx: Context = None) -> list[UserMessage]:
31
+ """Creates a new Google Slides presentation with the specified title."""
32
+ if ctx is None:
33
+ # Context should be automatically injected by the MCP framework
34
+ pass
35
+
36
+ prompt_message = UserMessage(
37
+ role="user",
38
+ content=f"Create a new Google Slides presentation with the title: {title}",
39
+ )
40
+ return [prompt_message]
@@ -0,0 +1,13 @@
1
+ """Resources for google-workspace-mcp."""
2
+
3
+ from .sheets_resources import (
4
+ get_specific_sheet_metadata_resource,
5
+ get_spreadsheet_metadata_resource,
6
+ )
7
+ from .slides import get_markdown_deck_formatting_guide
8
+
9
+ __all__ = [
10
+ "get_markdown_deck_formatting_guide",
11
+ "get_spreadsheet_metadata_resource",
12
+ "get_specific_sheet_metadata_resource",
13
+ ]
@@ -0,0 +1,79 @@
1
+ """
2
+ Calendar resources for Google Calendar data access.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from google_workspace_mcp.app import mcp
9
+ from google_workspace_mcp.services.calendar import CalendarService
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ # --- Calendar Resource Functions --- #
15
+
16
+
17
+ @mcp.resource("calendar://calendars/list")
18
+ async def list_calendars() -> dict[str, Any]:
19
+ """
20
+ Lists all calendars accessible by the user.
21
+
22
+ Maps to URI: calendar://calendars/list
23
+
24
+ Returns:
25
+ A dictionary containing the list of calendars or an error message.
26
+ """
27
+ logger.info("Executing list_calendars resource")
28
+
29
+ calendar_service = CalendarService()
30
+ calendars = calendar_service.list_calendars()
31
+
32
+ if isinstance(calendars, dict) and calendars.get("error"):
33
+ raise ValueError(calendars.get("message", "Error listing calendars"))
34
+
35
+ if not calendars:
36
+ return {"message": "No calendars found."}
37
+
38
+ # Return raw service result
39
+ return {"count": len(calendars), "calendars": calendars}
40
+
41
+
42
+ # Keep the existing list_calendars resource
43
+
44
+
45
+ @mcp.resource("calendar://events/today")
46
+ async def get_today_events() -> dict[str, Any]:
47
+ """
48
+ Get all events for today.
49
+
50
+ Maps to URI: calendar://events/today
51
+
52
+ Returns:
53
+ A dictionary containing the list of today's events.
54
+ """
55
+ logger.info("Executing get_today_events resource")
56
+
57
+ from datetime import datetime, timedelta
58
+
59
+ import pytz
60
+
61
+ calendar_service = CalendarService()
62
+
63
+ # Get today's start and end time in RFC3339 format
64
+ now = datetime.now(pytz.UTC)
65
+ start_of_day = datetime(now.year, now.month, now.day, 0, 0, 0, tzinfo=pytz.UTC)
66
+ end_of_day = start_of_day + timedelta(days=1)
67
+
68
+ time_min = start_of_day.isoformat()
69
+ time_max = end_of_day.isoformat()
70
+
71
+ events = calendar_service.get_events(calendar_id="primary", time_min=time_min, time_max=time_max, max_results=50)
72
+
73
+ if isinstance(events, dict) and events.get("error"):
74
+ raise ValueError(events.get("message", "Error getting today's events"))
75
+
76
+ if not events:
77
+ return {"message": "No events found for today."}
78
+
79
+ return {"count": len(events), "events": events}
@@ -0,0 +1,93 @@
1
+ """
2
+ Drive resources for Google Drive data access.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from google_workspace_mcp.app import mcp
9
+ from google_workspace_mcp.services.drive import DriveService
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ # --- Drive Resource Functions --- #
15
+
16
+
17
+ @mcp.resource("drive://recent")
18
+ async def get_recent_files() -> dict[str, Any]:
19
+ """
20
+ Get recently modified files (last 7 days).
21
+
22
+ Maps to URI: drive://recent
23
+
24
+ Returns:
25
+ A dictionary containing the list of recently modified files.
26
+ """
27
+ logger.info("Executing get_recent_files resource")
28
+
29
+ drive_service = DriveService()
30
+ # Using the existing search_files method with a fixed query
31
+ query = "modifiedTime > 'now-7d'"
32
+ files = drive_service.search_files(query=query, page_size=10)
33
+
34
+ if isinstance(files, dict) and files.get("error"):
35
+ raise ValueError(files.get("message", "Error getting recent files"))
36
+
37
+ if not files:
38
+ return {"message": "No recent files found."}
39
+
40
+ return {"count": len(files), "files": files}
41
+
42
+
43
+ @mcp.resource("drive://shared")
44
+ async def get_shared_files() -> dict[str, Any]:
45
+ """
46
+ Get files shared with the user.
47
+
48
+ Maps to URI: drive://shared
49
+
50
+ Returns:
51
+ A dictionary containing the list of shared files.
52
+ """
53
+ logger.info("Executing get_shared_files resource")
54
+
55
+ drive_service = DriveService()
56
+ # Using the existing search_files method with a fixed query
57
+ query = "sharedWithMe=true"
58
+ files = drive_service.search_files(query=query, page_size=10)
59
+
60
+ if isinstance(files, dict) and files.get("error"):
61
+ raise ValueError(files.get("message", "Error getting shared files"))
62
+
63
+ if not files:
64
+ return {"message": "No shared files found."}
65
+
66
+ return {"count": len(files), "files": files}
67
+
68
+
69
+ @mcp.resource("drive://files/{file_id}/metadata")
70
+ async def get_drive_file_metadata(file_id: str) -> dict[str, Any]:
71
+ """
72
+ Get metadata for a specific file from Google Drive.
73
+
74
+ Maps to URI: drive://files/{file_id}/metadata
75
+
76
+ Args:
77
+ file_id: The ID of the file to get metadata for
78
+
79
+ Returns:
80
+ A dictionary containing the file metadata.
81
+ """
82
+ logger.info(f"Executing get_drive_file_metadata resource for file_id: {file_id}")
83
+
84
+ if not file_id or not file_id.strip():
85
+ raise ValueError("File ID cannot be empty")
86
+
87
+ drive_service = DriveService()
88
+ metadata = drive_service.get_file_metadata(file_id=file_id)
89
+
90
+ if isinstance(metadata, dict) and metadata.get("error"):
91
+ raise ValueError(metadata.get("message", "Error getting file metadata"))
92
+
93
+ return metadata
@@ -0,0 +1,58 @@
1
+ """
2
+ Gmail resources for Gmail data access.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from google_workspace_mcp.app import mcp
9
+ from google_workspace_mcp.services.gmail import GmailService
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ # --- Gmail Resource Functions --- #
15
+
16
+
17
+ @mcp.resource("gmail://labels")
18
+ async def get_gmail_labels() -> dict[str, Any]:
19
+ """
20
+ List all Gmail labels for the authenticated user.
21
+
22
+ Maps to URI: gmail://labels
23
+
24
+ Returns:
25
+ A dictionary containing the list of Gmail labels.
26
+ """
27
+ logger.info("Executing get_gmail_labels resource")
28
+
29
+ gmail_service = GmailService()
30
+ # This would require adding a get_labels method to GmailService
31
+ labels = gmail_service.get_labels()
32
+
33
+ if isinstance(labels, dict) and labels.get("error"):
34
+ raise ValueError(labels.get("message", "Error getting Gmail labels"))
35
+
36
+ return {"count": len(labels), "labels": labels}
37
+
38
+
39
+ @mcp.resource("gmail://unread_count")
40
+ async def get_unread_count() -> dict[str, Any]:
41
+ """
42
+ Get count of unread emails in the inbox.
43
+
44
+ Maps to URI: gmail://unread_count
45
+
46
+ Returns:
47
+ A dictionary containing the count of unread emails.
48
+ """
49
+ logger.info("Executing get_unread_count resource")
50
+
51
+ gmail_service = GmailService()
52
+ # This would require adding a get_unread_count method to GmailService
53
+ result = gmail_service.get_unread_count()
54
+
55
+ if isinstance(result, dict) and result.get("error"):
56
+ raise ValueError(result.get("message", "Error getting unread count"))
57
+
58
+ return {"unread_count": result}