#
tokens: 35295/50000 15/17 files (page 1/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 3. Use http://codebase.md/markuspfundstein/mcp-gsuite?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── .python-version
├── Dockerfile
├── gmail-api-openapi-spec.yaml
├── gmail.v1.json
├── google-calendar-api-openapi-spec.yaml
├── LICENSE
├── pyproject.toml
├── README.md
├── smithery.yaml
├── src
│   └── mcp_gsuite
│       ├── __init__.py
│       ├── calendar.py
│       ├── gauth.py
│       ├── gmail.py
│       ├── server.py
│       ├── toolhandler.py
│       ├── tools_calendar.py
│       └── tools_gmail.py
└── uv.lock
```

# Files

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

```
1 | 3.13
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 | .env
12 | .gauth.json
13 | oauth2creds.json
14 | .accounts.json
15 | .oauth2.*.json
16 | 
```

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

```markdown
  1 | # mcp-gsuite MCP server
  2 | 
  3 | [![smithery badge](https://smithery.ai/badge/mcp-gsuite)](https://smithery.ai/server/mcp-gsuite)
  4 | MCP server to interact with Google products.
  5 | 
  6 | ## Example prompts
  7 | 
  8 | Right now, this MCP server supports Gmail and Calendar integration with the following capabilities:
  9 | 
 10 | 1. General
 11 | * Multiple google accounts
 12 | 
 13 | 2. Gmail
 14 | * Get your Gmail user information
 15 | * Query emails with flexible search (e.g., unread, from specific senders, date ranges, with attachments)
 16 | * Retrieve complete email content by ID
 17 | * Create new draft emails with recipients, subject, body and CC options
 18 | * Delete draft emails
 19 | * Reply to existing emails (can either send immediately or save as draft)
 20 | * Retrieve multiple emails at once by their IDs.
 21 | * Save multiple attachments from emails to your local system.
 22 | 
 23 | 3. Calendar
 24 | * Manage multiple calendars
 25 | * Get calendar events within specified time ranges
 26 | * Create calendar events with:
 27 |   + Title, start/end times
 28 |   + Optional location and description
 29 |   + Optional attendees
 30 |   + Custom timezone support
 31 |   + Notification preferences
 32 | * Delete calendar events
 33 | 
 34 | Example prompts you can try:
 35 | 
 36 | * Retrieve my latest unread messages
 37 | * Search my emails from the Scrum Master
 38 | * Retrieve all emails from accounting
 39 | * Take the email about ABC and summarize it
 40 | * Write a nice response to Alice's last email and upload a draft.
 41 | * Reply to Bob's email with a Thank you note. Store it as draft
 42 | 
 43 | * What do I have on my agenda tomorrow?
 44 | * Check my private account's Family agenda for next week
 45 | * I need to plan an event with Tim for 2hrs next week. Suggest some time slots.
 46 | 
 47 | ## Quickstart
 48 | 
 49 | ### Install
 50 | 
 51 | ### Installing via Smithery
 52 | 
 53 | To install mcp-gsuite for Claude Desktop automatically via [Smithery](https://smithery.ai/server/mcp-gsuite):
 54 | 
 55 | ```bash
 56 | npx -y @smithery/cli install mcp-gsuite --client claude
 57 | ```
 58 | 
 59 | #### Oauth 2
 60 | 
 61 | Google Workspace (G Suite) APIs require OAuth2 authorization. Follow these steps to set up authentication:
 62 | 
 63 | 1. Create OAuth2 Credentials:
 64 |    - Go to the [Google Cloud Console](https://console.cloud.google.com/)
 65 |    - Create a new project or select an existing one
 66 |    - Enable the Gmail API and Google Calendar API for your project
 67 |    - Go to "Credentials" → "Create Credentials" → "OAuth client ID"
 68 |    - Select "Desktop app" or "Web application" as the application type
 69 |    - Configure the OAuth consent screen with required information
 70 |    - Add authorized redirect URIs (include `http://localhost:4100/code` for local development)
 71 | 
 72 | 2. Required OAuth2 Scopes:
 73 |    
 74 | 
 75 | ```json
 76 |    [
 77 |      "openid",
 78 |      "https://mail.google.com/",
 79 |      "https://www.googleapis.com/auth/calendar",
 80 |      "https://www.googleapis.com/auth/userinfo.email"
 81 |    ]
 82 | ```
 83 | 
 84 | 3. Then create a `.gauth.json` in your working directory with client
 85 | 
 86 | ```json
 87 | {
 88 |     "web": {
 89 |         "client_id": "$your_client_id",
 90 |         "client_secret": "$your_client_secret",
 91 |         "redirect_uris": ["http://localhost:4100/code"],
 92 |         "auth_uri": "https://accounts.google.com/o/oauth2/auth",
 93 |         "token_uri": "https://oauth2.googleapis.com/token"
 94 |     }
 95 | }
 96 | ```
 97 | 
 98 | 4. Create a `.accounts.json` file with account information
 99 | 
100 | ```json
101 | {
102 |     "accounts": [
103 |         {
104 |             "email": "[email protected]",
105 |             "account_type": "personal",
106 |             "extra_info": "Additional info that you want to tell Claude: E.g. 'Contains Family Calendar'"
107 |         }
108 |     ]
109 | }
110 | ```
111 | 
112 | You can specifiy multiple accounts. Make sure they have access in your Google Auth app. The `extra_info` field is especially interesting as you can add info here that you want to tell the AI about the account (e.g. whether it has a specific agenda)
113 | 
114 | Note: When you first execute one of the tools for a specific account, a browser will open, redirect you to Google and ask for your credentials, scope, etc. After a successful login, it stores the credentials in a local file called `.oauth.{email}.json` . Once you are authorized, the refresh token will be used.
115 | 
116 | #### Claude Desktop
117 | 
118 | On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
119 | 
120 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
121 | 
122 | <details>
123 |   <summary>Development/Unpublished Servers Configuration</summary>
124 |   
125 | 
126 | ```json
127 | {
128 |   "mcpServers": {
129 |     "mcp-gsuite": {
130 |       "command": "uv",
131 |       "args": [
132 |         "--directory",
133 |         "<dir_to>/mcp-gsuite",
134 |         "run",
135 |         "mcp-gsuite"
136 |       ]
137 |     }
138 |   }
139 | }
140 | ```
141 | 
142 | 
143 | Note: You can also use the `uv run mcp-gsuite --accounts-file /path/to/custom/.accounts.json` to specify a different accounts file or `--credentials-dir /path/to/custom/credentials` to specify a different credentials directory.
144 | 
145 | ```json
146 | {
147 |   "mcpServers": {
148 |     "mcp-gsuite": {
149 |       "command": "uv",
150 |       "args": [
151 |         "--directory",
152 |         "<dir_to>/mcp-gsuite",
153 |         "run",
154 |         "mcp-gsuite",
155 |         "--accounts-file",
156 |         "/path/to/custom/.accounts.json",
157 |         "--credentials-dir",
158 |         "/path/to/custom/credentials"
159 |       ]
160 |     }
161 |   }
162 | }
163 | ```
164 | 
165 | </details>
166 | 
167 | <details>
168 |   <summary>Published Servers Configuration</summary>
169 |   
170 | 
171 | ```json
172 | {
173 |   "mcpServers": {
174 |     "mcp-gsuite": {
175 |       "command": "uvx",
176 |       "args": [
177 |         "mcp-gsuite",
178 |         "--accounts-file",
179 |         "/path/to/custom/.accounts.json",
180 |         "--credentials-dir",
181 |         "/path/to/custom/credentials"
182 |       ]
183 |     }
184 |   }
185 | }
186 | ```
187 | 
188 | </details>
189 | 
190 | ### Configuration Options
191 | 
192 | The MCP server can be configured with several command-line options to specify custom paths for authentication and account information:
193 | 
194 | * `--gauth-file`: Specifies the path to the `.gauth.json` file containing OAuth2 client configuration. Default is `./.gauth.json`.
195 | * `--accounts-file`: Specifies the path to the `.accounts.json` file containing information about the Google accounts. Default is `./.accounts.json`.
196 | * `--credentials-dir`: Specifies the directory where OAuth credentials are stored after successful authentication. Default is the current working directory with a subdirectory for each account as `.oauth.{email}.json`.
197 | 
198 | These options allow for flexibility in managing different environments or multiple sets of credentials and accounts, especially useful in development and testing scenarios.
199 | 
200 | Example usage:
201 | 
202 | ```bash
203 | uv run mcp-gsuite --gauth-file /path/to/custom/.gauth.json --accounts-file /path/to/custom/.accounts.json --credentials-dir /path/to/custom/credentials
204 | ```
205 | 
206 | This configuration is particularly useful when you have multiple instances of the server running with different configurations or when deploying to environments where the default paths are not suitable.
207 | 
208 | ## Development
209 | 
210 | ### Building and Publishing
211 | 
212 | To prepare the package for distribution:
213 | 
214 | 1. Sync dependencies and update lockfile:
215 | 
216 | ```bash
217 | uv sync
218 | ```
219 | 
220 | 2. Build package distributions:
221 | 
222 | ```bash
223 | uv build
224 | ```
225 | 
226 | This will create source and wheel distributions in the `dist/` directory.
227 | 
228 | 3. Publish to PyPI:
229 | 
230 | ```bash
231 | uv publish
232 | ```
233 | 
234 | Note: You'll need to set PyPI credentials via environment variables or command flags:
235 | * Token: `--token` or `UV_PUBLISH_TOKEN`
236 | * Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD`
237 | 
238 | ### Debugging
239 | 
240 | Since MCP servers run over stdio, debugging can be challenging. For the best debugging
241 | experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
242 | 
243 | You can launch the MCP Inspector via [ `npm` ](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
244 | 
245 | ```bash
246 | npx @modelcontextprotocol/inspector uv --directory /path/to/mcp-gsuite run mcp-gsuite
247 | ```
248 | 
249 | Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
250 | 
251 | You can also watch the server logs with this command:
252 | 
253 | ```bash
254 | tail -n 20 -f ~/Library/Logs/Claude/mcp-server-mcp-gsuite.log
255 | ```
256 | 
```

--------------------------------------------------------------------------------
/src/mcp_gsuite/__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 = "mcp-gsuite"
 3 | version = "0.4.1"
 4 | description = "MCP Server to connect to Google G-Suite"
 5 | readme = "README.md"
 6 | requires-python = ">=3.13"
 7 | dependencies = [
 8 |  "beautifulsoup4>=4.12.3",
 9 |  "google-api-python-client>=2.154.0",
10 |  "httplib2>=0.22.0",
11 |  "mcp>=1.3.0",
12 |  "oauth2client==4.1.3",
13 |  "python-dotenv>=1.0.1",
14 |  "pytz>=2024.2",
15 |  "requests>=2.32.3",
16 | ]
17 | [[project.authors]]
18 | name = "Markus Pfundstein"
19 | email = "[email protected]"
20 | 
21 | [build-system]
22 | requires = [ "hatchling",]
23 | build-backend = "hatchling.build"
24 | 
25 | [dependency-groups]
26 | dev = [
27 |     "pyright>=1.1.389",
28 | ]
29 | 
30 | [project.scripts]
31 | mcp-gsuite = "mcp_gsuite:main"
32 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - gauthFile
10 |       - accountsFile
11 |     properties:
12 |       gauthFile:
13 |         type: string
14 |         description: Path to the OAuth2 client configuration file.
15 |       accountsFile:
16 |         type: string
17 |         description: Path to the Google accounts configuration file.
18 |       credentialsDir:
19 |         type: string
20 |         description: Directory where OAuth credentials are stored.
21 |   commandFunction:
22 |     # A function that produces the CLI command to start the MCP on stdio.
23 |     |-
24 |     (config) => ({command: 'uv', args: ['run', 'mcp-gsuite', '--gauth-file', config.gauthFile, '--accounts-file', config.accountsFile, '--credentials-dir', config.credentialsDir]})
25 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | # Use a Python image with uv pre-installed
 3 | FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS uv
 4 | 
 5 | # Set the working directory
 6 | WORKDIR /app
 7 | 
 8 | # Copy necessary configuration files
 9 | COPY . .
10 | 
11 | # Enable bytecode compilation
12 | ENV UV_COMPILE_BYTECODE=1
13 | 
14 | # Use the copy link mode for mount points
15 | ENV UV_LINK_MODE=copy
16 | 
17 | # Sync dependencies and build the project
18 | RUN --mount=type=cache,target=/root/.cache/uv     --mount=type=bind,source=uv.lock,target=uv.lock     --mount=type=bind,source=pyproject.toml,target=pyproject.toml     uv sync --frozen --no-install-project --no-dev --no-editable
19 | 
20 | # Install the project
21 | RUN --mount=type=cache,target=/root/.cache/uv     uv sync --frozen --no-dev --no-editable
22 | 
23 | # Final stage: running the application
24 | FROM python:3.13-slim-bookworm
25 | 
26 | WORKDIR /app
27 | 
28 | COPY --from=uv /root/.local /root/.local
29 | COPY --from=uv --chown=app:app /app/.venv /app/.venv
30 | 
31 | # Place executables in the environment at the front of the path
32 | ENV PATH="/app/.venv/bin:$PATH"
33 | 
34 | # Expose necessary ports
35 | EXPOSE 4100
36 | 
37 | # Specify the entrypoint command
38 | ENTRYPOINT ["uv", "run", "mcp-gsuite"]
39 | 
```

--------------------------------------------------------------------------------
/src/mcp_gsuite/toolhandler.py:
--------------------------------------------------------------------------------

```python
 1 | from collections.abc import Sequence
 2 | from mcp.types import (
 3 |     Tool,
 4 |     TextContent,
 5 |     ImageContent,
 6 |     EmbeddedResource,
 7 | )
 8 | 
 9 | from . import gauth
10 | 
11 | USER_ID_ARG = "__user_id__"
12 | 
13 | class ToolHandler():
14 |     def __init__(self, tool_name: str):
15 |         self.name = tool_name
16 | 
17 |     def get_account_descriptions(self) -> list[str]:
18 |         return [a.to_description() for a in gauth.get_account_info()]
19 |     
20 |     # we ingest this information into every tool that requires a specified __user_id__. 
21 |     # we also add what information actually can be used (account info). This way Claude
22 |     # will know what to do.
23 |     def get_supported_emails_tool_text(self) -> str:
24 |         return f"""This tool requires a authorized Google account email for {USER_ID_ARG} argument. You can choose one of: {', '.join(self.get_account_descriptions())}"""
25 | 
26 |     def get_user_id_arg_schema(self) -> dict:
27 |         return {
28 |             "type": "string",
29 |             "description": f"The EMAIL of the Google account for which you are executing this action. Can be one of: {', '.join(self.get_account_descriptions())}"
30 |         }
31 | 
32 |     def get_tool_description(self) -> Tool:
33 |         raise NotImplementedError()
34 | 
35 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
36 |         raise NotImplementedError()
```

--------------------------------------------------------------------------------
/src/mcp_gsuite/server.py:
--------------------------------------------------------------------------------

```python
  1 | 
  2 | import logging
  3 | from collections.abc import Sequence
  4 | from functools import lru_cache
  5 | import subprocess
  6 | from typing import Any
  7 | import traceback
  8 | from dotenv import load_dotenv
  9 | from mcp.server import Server
 10 | import threading
 11 | import sys
 12 | from mcp.types import (
 13 |     Tool,
 14 |     TextContent,
 15 |     ImageContent,
 16 |     EmbeddedResource,
 17 | )
 18 | import json
 19 | from . import gauth
 20 | from http.server import BaseHTTPRequestHandler,HTTPServer
 21 | from urllib.parse import (
 22 |     urlparse,
 23 |     parse_qs,
 24 | )
 25 | 
 26 | class OauthListener(BaseHTTPRequestHandler):
 27 |     def do_GET(self):
 28 |         url = urlparse(self.path)
 29 |         if url.path != "/code":
 30 |             self.send_response(404)
 31 |             self.end_headers()
 32 |             return
 33 | 
 34 |         query = parse_qs(url.query)
 35 |         if "code" not in query:
 36 |             self.send_response(400)
 37 |             self.end_headers()
 38 |             return
 39 |         
 40 |         self.send_response(200)
 41 |         self.end_headers()
 42 |         self.wfile.write("Auth successful! You can close the tab!".encode("utf-8"))
 43 |         self.wfile.flush()
 44 | 
 45 |         storage = {}
 46 |         creds = gauth.get_credentials(authorization_code=query["code"][0], state=storage)
 47 | 
 48 |         t = threading.Thread(target = self.server.shutdown)
 49 |         t.daemon = True
 50 |         t.start()
 51 | 
 52 |         
 53 | 
 54 | load_dotenv()
 55 | 
 56 | from . import tools_gmail
 57 | from . import tools_calendar
 58 | from . import toolhandler
 59 | 
 60 | 
 61 | # Load environment variables
 62 | 
 63 | # Configure logging
 64 | logging.basicConfig(level=logging.INFO)
 65 | logger = logging.getLogger("mcp-gsuite")
 66 | 
 67 | def start_auth_flow(user_id: str):
 68 |     auth_url = gauth.get_authorization_url(user_id, state={})
 69 |     if sys.platform == "darwin" or sys.platform.startswith("linux"):
 70 |         subprocess.Popen(['open', auth_url])
 71 |     else:
 72 |         import webbrowser
 73 |         webbrowser.open(auth_url)
 74 | 
 75 |     # start server for code callback
 76 |     server_address = ('', 4100)
 77 |     server = HTTPServer(server_address, OauthListener)
 78 |     server.serve_forever()
 79 | 
 80 | 
 81 | def setup_oauth2(user_id: str):
 82 |     accounts = gauth.get_account_info()
 83 |     if len(accounts) == 0:
 84 |         raise RuntimeError("No accounts specified in .gauth.json")
 85 |     if user_id not in [a.email for a in accounts]:
 86 |         raise RuntimeError(f"Account for email: {user_id} not specified in .gauth.json")
 87 | 
 88 |     credentials = gauth.get_stored_credentials(user_id=user_id)
 89 |     if not credentials:
 90 |         start_auth_flow(user_id=user_id)
 91 |     else:
 92 |         if credentials.access_token_expired:
 93 |             logger.error("credentials expired. try refresh")
 94 | 
 95 |         # this call refreshes access token
 96 |         user_info = gauth.get_user_info(credentials=credentials)
 97 |         #logging.error(f"User info: {json.dumps(user_info)}")
 98 |         gauth.store_credentials(credentials=credentials, user_id=user_id)
 99 | 
100 | 
101 | app = Server("mcp-gsuite")
102 | 
103 | tool_handlers = {}
104 | def add_tool_handler(tool_class: toolhandler.ToolHandler):
105 |     global tool_handlers
106 | 
107 |     tool_handlers[tool_class.name] = tool_class
108 | 
109 | def get_tool_handler(name: str) -> toolhandler.ToolHandler | None:
110 |     if name not in tool_handlers:
111 |         return None
112 |     
113 |     return tool_handlers[name]
114 | 
115 | add_tool_handler(tools_gmail.QueryEmailsToolHandler())
116 | add_tool_handler(tools_gmail.GetEmailByIdToolHandler())
117 | add_tool_handler(tools_gmail.CreateDraftToolHandler())
118 | add_tool_handler(tools_gmail.DeleteDraftToolHandler())
119 | add_tool_handler(tools_gmail.ReplyEmailToolHandler())
120 | add_tool_handler(tools_gmail.GetAttachmentToolHandler())
121 | add_tool_handler(tools_gmail.BulkGetEmailsByIdsToolHandler())
122 | add_tool_handler(tools_gmail.BulkSaveAttachmentsToolHandler())
123 | 
124 | add_tool_handler(tools_calendar.ListCalendarsToolHandler())
125 | add_tool_handler(tools_calendar.GetCalendarEventsToolHandler())
126 | add_tool_handler(tools_calendar.CreateCalendarEventToolHandler())
127 | add_tool_handler(tools_calendar.DeleteCalendarEventToolHandler())
128 | 
129 | @app.list_tools()
130 | async def list_tools() -> list[Tool]:
131 |     """List available tools."""
132 | 
133 |     return [th.get_tool_description() for th in tool_handlers.values()]
134 | 
135 | 
136 | @app.call_tool()
137 | async def call_tool(name: str, arguments: Any) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
138 |     try:        
139 |         if not isinstance(arguments, dict):
140 |             raise RuntimeError("arguments must be dictionary")
141 |         
142 |         if toolhandler.USER_ID_ARG not in arguments:
143 |             raise RuntimeError("user_id argument is missing in dictionary.")
144 | 
145 |         setup_oauth2(user_id=arguments.get(toolhandler.USER_ID_ARG, ""))
146 | 
147 |         tool_handler = get_tool_handler(name)
148 |         if not tool_handler:
149 |             raise ValueError(f"Unknown tool: {name}")
150 | 
151 |         return tool_handler.run_tool(arguments)
152 |     except Exception as e:
153 |         logging.error(traceback.format_exc())
154 |         logging.error(f"Error during call_tool: str(e)")
155 |         raise RuntimeError(f"Caught Exception. Error: {str(e)}")
156 | 
157 | 
158 | async def main():
159 |     print(sys.platform)
160 |     accounts = gauth.get_account_info()
161 |     for account in accounts:
162 |         creds = gauth.get_stored_credentials(user_id=account.email)
163 |         if creds:
164 |             logging.info(f"found credentials for {account.email}")
165 | 
166 |     from mcp.server.stdio import stdio_server
167 | 
168 |     async with stdio_server() as (read_stream, write_stream):
169 |         await app.run(
170 |             read_stream,
171 |             write_stream,
172 |             app.create_initialization_options()
173 |         )
```

--------------------------------------------------------------------------------
/src/mcp_gsuite/calendar.py:
--------------------------------------------------------------------------------

```python
  1 | from googleapiclient.discovery import build
  2 | from . import gauth
  3 | import logging
  4 | import traceback
  5 | from datetime import datetime
  6 | import pytz
  7 | 
  8 | class CalendarService():
  9 |     def __init__(self, user_id: str):
 10 |         credentials = gauth.get_stored_credentials(user_id=user_id)
 11 |         if not credentials:
 12 |             raise RuntimeError("No Oauth2 credentials stored")
 13 |         self.service = build('calendar', 'v3', credentials=credentials)  # Note: using v3 for Calendar API
 14 |     
 15 |     def list_calendars(self) -> list:
 16 |         """
 17 |         Lists all calendars accessible by the user.
 18 |         
 19 |         Returns:
 20 |             list: List of calendar objects with their metadata
 21 |         """
 22 |         try:
 23 |             calendar_list = self.service.calendarList().list().execute()
 24 | 
 25 |             calendars = []
 26 |             
 27 |             for calendar in calendar_list.get('items', []):
 28 |                 if calendar.get('kind') == 'calendar#calendarListEntry':
 29 |                     calendars.append({
 30 |                         'id': calendar.get('id'),
 31 |                         'summary': calendar.get('summary'),
 32 |                         'primary': calendar.get('primary', False),
 33 |                         'time_zone': calendar.get('timeZone'),
 34 |                         'etag': calendar.get('etag'),
 35 |                         'access_role': calendar.get('accessRole')
 36 |                     })
 37 | 
 38 |             return calendars
 39 |                 
 40 |         except Exception as e:
 41 |             logging.error(f"Error retrieving calendars: {str(e)}")
 42 |             logging.error(traceback.format_exc())
 43 |             return []
 44 | 
 45 |     def get_events(self, time_min=None, time_max=None, max_results=250, show_deleted=False, calendar_id: str ='primary'):
 46 |         """
 47 |         Retrieve calendar events within a specified time range.
 48 |         
 49 |         Args:
 50 |             time_min (str, optional): Start time in RFC3339 format. Defaults to current time.
 51 |             time_max (str, optional): End time in RFC3339 format
 52 |             max_results (int): Maximum number of events to return (1-2500)
 53 |             show_deleted (bool): Whether to include deleted events
 54 |             
 55 |         Returns:
 56 |             list: List of calendar events
 57 |         """
 58 |         try:
 59 |             # If no time_min specified, use current time
 60 |             if not time_min:
 61 |                 time_min = datetime.now(pytz.UTC).isoformat()
 62 |                 
 63 |             # Ensure max_results is within limits
 64 |             max_results = min(max(1, max_results), 2500)
 65 |             
 66 |             # Prepare parameters
 67 |             params = {
 68 |                 'calendarId': calendar_id,
 69 |                 'timeMin': time_min,
 70 |                 'maxResults': max_results,
 71 |                 'singleEvents': True,
 72 |                 'orderBy': 'startTime',
 73 |                 'showDeleted': show_deleted
 74 |             }
 75 |             
 76 |             # Add optional time_max if specified
 77 |             if time_max:
 78 |                 params['timeMax'] = time_max
 79 |                 
 80 |             # Execute the events().list() method
 81 |             events_result = self.service.events().list(**params).execute()
 82 |             
 83 |             # Extract the events
 84 |             events = events_result.get('items', [])
 85 |             
 86 |             # Process and return the events
 87 |             processed_events = []
 88 |             for event in events:
 89 |                 processed_event = {
 90 |                     'id': event.get('id'),
 91 |                     'summary': event.get('summary'),
 92 |                     'description': event.get('description'),
 93 |                     'start': event.get('start'),
 94 |                     'end': event.get('end'),
 95 |                     'status': event.get('status'),
 96 |                     'creator': event.get('creator'),
 97 |                     'organizer': event.get('organizer'),
 98 |                     'attendees': event.get('attendees'),
 99 |                     'location': event.get('location'),
100 |                     'hangoutLink': event.get('hangoutLink'),
101 |                     'conferenceData': event.get('conferenceData'),
102 |                     'recurringEventId': event.get('recurringEventId')
103 |                 }
104 |                 processed_events.append(processed_event)
105 |                 
106 |             return processed_events
107 |             
108 |         except Exception as e:
109 |             logging.error(f"Error retrieving calendar events: {str(e)}")
110 |             logging.error(traceback.format_exc())
111 |             return []
112 |         
113 |     def create_event(self, summary: str, start_time: str, end_time: str, 
114 |                 location: str | None = None, description: str | None = None, 
115 |                 attendees: list | None = None, send_notifications: bool = True,
116 |                 timezone: str | None = None,
117 |                 calendar_id : str = 'primary') -> dict | None:
118 |         """
119 |         Create a new calendar event.
120 |         
121 |         Args:
122 |             summary (str): Title of the event
123 |             start_time (str): Start time in RFC3339 format
124 |             end_time (str): End time in RFC3339 format
125 |             location (str, optional): Location of the event
126 |             description (str, optional): Description of the event
127 |             attendees (list, optional): List of attendee email addresses
128 |             send_notifications (bool): Whether to send notifications to attendees
129 |             timezone (str, optional): Timezone for the event (e.g. 'America/New_York')
130 |             
131 |         Returns:
132 |             dict: Created event data or None if creation fails
133 |         """
134 |         try:
135 |             # Prepare event data
136 |             event = {
137 |                 'summary': summary,
138 |                 'start': {
139 |                     'dateTime': start_time,
140 |                     'timeZone': timezone or 'UTC',
141 |                 },
142 |                 'end': {
143 |                     'dateTime': end_time,
144 |                     'timeZone': timezone or 'UTC',
145 |                 }
146 |             }
147 |             
148 |             # Add optional fields if provided
149 |             if location:
150 |                 event['location'] = location
151 |             if description:
152 |                 event['description'] = description
153 |             if attendees:
154 |                 event['attendees'] = [{'email': email} for email in attendees]
155 |                 
156 |             # Create the event
157 |             created_event = self.service.events().insert(
158 |                 calendarId=calendar_id,
159 |                 body=event,
160 |                 sendNotifications=send_notifications
161 |             ).execute()
162 |             
163 |             return created_event
164 |             
165 |         except Exception as e:
166 |             logging.error(f"Error creating calendar event: {str(e)}")
167 |             logging.error(traceback.format_exc())
168 |             return None
169 |         
170 |     def delete_event(self, event_id: str, send_notifications: bool = True, calendar_id: str = 'primary') -> bool:
171 |         """
172 |         Delete a calendar event by its ID.
173 |         
174 |         Args:
175 |             event_id (str): The ID of the event to delete
176 |             send_notifications (bool): Whether to send cancellation notifications to attendees
177 |             
178 |         Returns:
179 |             bool: True if deletion was successful, False otherwise
180 |         """
181 |         try:
182 |             self.service.events().delete(
183 |                 calendarId=calendar_id,
184 |                 eventId=event_id,
185 |                 sendNotifications=send_notifications
186 |             ).execute()
187 |             return True
188 |             
189 |         except Exception as e:
190 |             logging.error(f"Error deleting calendar event {event_id}: {str(e)}")
191 |             logging.error(traceback.format_exc())
192 |             return False
```

--------------------------------------------------------------------------------
/src/mcp_gsuite/gauth.py:
--------------------------------------------------------------------------------

```python
  1 | import logging
  2 | from oauth2client.client import (
  3 |     flow_from_clientsecrets,
  4 |     FlowExchangeError,
  5 |     OAuth2Credentials,
  6 |     Credentials,
  7 | )
  8 | from googleapiclient.discovery import build
  9 | import httplib2
 10 | from google.auth.transport.requests import Request
 11 | import os
 12 | import pydantic
 13 | import json
 14 | import argparse
 15 | 
 16 | 
 17 | def get_gauth_file() -> str:
 18 |     parser = argparse.ArgumentParser()
 19 |     parser.add_argument(
 20 |         "--gauth-file",
 21 |         type=str,
 22 |         default="./.gauth.json",
 23 |         help="Path to client secrets file",
 24 |     )
 25 |     args, _ = parser.parse_known_args()
 26 |     return args.gauth_file
 27 | 
 28 | 
 29 | CLIENTSECRETS_LOCATION = get_gauth_file()
 30 | 
 31 | REDIRECT_URI = 'http://localhost:4100/code'
 32 | SCOPES = [
 33 |     "openid",
 34 |     "https://www.googleapis.com/auth/userinfo.email",
 35 |     "https://mail.google.com/",
 36 |     "https://www.googleapis.com/auth/calendar"
 37 | ]
 38 | 
 39 | 
 40 | class AccountInfo(pydantic.BaseModel):
 41 | 
 42 |     email: str
 43 |     account_type: str
 44 |     extra_info: str
 45 | 
 46 |     def __init__(self, email: str, account_type: str, extra_info: str = ""):
 47 |         super().__init__(email=email, account_type=account_type, extra_info=extra_info)
 48 | 
 49 |     def to_description(self):
 50 |         return f"""Account for email: {self.email} of type: {self.account_type}. Extra info for: {self.extra_info}"""
 51 | 
 52 | 
 53 | def get_accounts_file() -> str:
 54 |     parser = argparse.ArgumentParser()
 55 |     parser.add_argument(
 56 |         "--accounts-file",
 57 |         type=str,
 58 |         default="./.accounts.json",
 59 |         help="Path to accounts configuration file",
 60 |     )
 61 |     args, _ = parser.parse_known_args()
 62 |     return args.accounts_file
 63 | 
 64 | 
 65 | def get_account_info() -> list[AccountInfo]:
 66 |     accounts_file = get_accounts_file()
 67 |     with open(accounts_file) as f:
 68 |         data = json.load(f)
 69 |         accounts = data.get("accounts", [])
 70 |         return [AccountInfo.model_validate(acc) for acc in accounts]
 71 | 
 72 | class GetCredentialsException(Exception):
 73 |   """Error raised when an error occurred while retrieving credentials.
 74 | 
 75 |   Attributes:
 76 |     authorization_url: Authorization URL to redirect the user to in order to
 77 |                        request offline access.
 78 |   """
 79 | 
 80 |   def __init__(self, authorization_url):
 81 |     """Construct a GetCredentialsException."""
 82 |     self.authorization_url = authorization_url
 83 | 
 84 | 
 85 | class CodeExchangeException(GetCredentialsException):
 86 |   """Error raised when a code exchange has failed."""
 87 | 
 88 | 
 89 | class NoRefreshTokenException(GetCredentialsException):
 90 |   """Error raised when no refresh token has been found."""
 91 | 
 92 | 
 93 | class NoUserIdException(Exception):
 94 |   """Error raised when no user ID could be retrieved."""
 95 | 
 96 | 
 97 | def get_credentials_dir() -> str:
 98 |     parser = argparse.ArgumentParser()
 99 |     parser.add_argument(
100 |         "--credentials-dir",
101 |         type=str,
102 |         default=".",
103 |         help="Directory to store OAuth2 credentials",
104 |     )
105 |     args, _ = parser.parse_known_args()
106 |     return args.credentials_dir
107 | 
108 | 
109 | def _get_credential_filename(user_id: str) -> str:
110 |     creds_dir = get_credentials_dir()
111 |     return os.path.join(creds_dir, f".oauth2.{user_id}.json")
112 | 
113 | 
114 | def get_stored_credentials(user_id: str) -> OAuth2Credentials | None:
115 |     """Retrieved stored credentials for the provided user ID.
116 | 
117 |     Args:
118 |     user_id: User's ID.
119 |     Returns:
120 |     Stored oauth2client.client.OAuth2Credentials if found, None otherwise.
121 |     """
122 |     try:
123 | 
124 |         cred_file_path = _get_credential_filename(user_id=user_id)
125 |         if not os.path.exists(cred_file_path):
126 |             logging.warning(f"No stored Oauth2 credentials yet at path: {cred_file_path}")
127 |             return None
128 | 
129 |         with open(cred_file_path, 'r') as f:
130 |             data = f.read()
131 |             return Credentials.new_from_json(data)
132 |     except Exception as e:
133 |         logging.error(e)
134 |         return None
135 | 
136 |     raise None
137 | 
138 | 
139 | def store_credentials(credentials: OAuth2Credentials, user_id: str):
140 |     """Store OAuth 2.0 credentials in the specified directory."""
141 |     cred_file_path = _get_credential_filename(user_id=user_id)
142 |     os.makedirs(os.path.dirname(cred_file_path), exist_ok=True)
143 |     
144 |     data = credentials.to_json()
145 |     with open(cred_file_path, "w") as f:
146 |         f.write(data)
147 | 
148 | 
149 | def exchange_code(authorization_code):
150 |     """Exchange an authorization code for OAuth 2.0 credentials.
151 | 
152 |     Args:
153 |     authorization_code: Authorization code to exchange for OAuth 2.0
154 |                         credentials.
155 |     Returns:
156 |     oauth2client.client.OAuth2Credentials instance.
157 |     Raises:
158 |     CodeExchangeException: an error occurred.
159 |     """
160 |     flow = flow_from_clientsecrets(CLIENTSECRETS_LOCATION, ' '.join(SCOPES))
161 |     flow.redirect_uri = REDIRECT_URI
162 |     try:
163 |         credentials = flow.step2_exchange(authorization_code)
164 |         return credentials
165 |     except FlowExchangeError as error:
166 |         logging.error('An error occurred: %s', error)
167 |         raise CodeExchangeException(None)
168 | 
169 | 
170 | def get_user_info(credentials):
171 |     """Send a request to the UserInfo API to retrieve the user's information.
172 | 
173 |     Args:
174 |     credentials: oauth2client.client.OAuth2Credentials instance to authorize the
175 |                     request.
176 |     Returns:
177 |     User information as a dict.
178 |     """
179 |     user_info_service = build(
180 |         serviceName='oauth2', version='v2',
181 |         http=credentials.authorize(httplib2.Http()))
182 |     user_info = None
183 |     try:
184 |         user_info = user_info_service.userinfo().get().execute()
185 |     except Exception as e:
186 |         logging.error(f'An error occurred: {e}')
187 |     if user_info and user_info.get('id'):
188 |         return user_info
189 |     else:
190 |         raise NoUserIdException()
191 | 
192 | 
193 | def get_authorization_url(email_address, state):
194 |     """Retrieve the authorization URL.
195 | 
196 |     Args:
197 |     email_address: User's e-mail address.
198 |     state: State for the authorization URL.
199 |     Returns:
200 |     Authorization URL to redirect the user to.
201 |     """
202 |     flow = flow_from_clientsecrets(CLIENTSECRETS_LOCATION, ' '.join(SCOPES), redirect_uri=REDIRECT_URI)
203 |     flow.params['access_type'] = 'offline'
204 |     flow.params['approval_prompt'] = 'force'
205 |     flow.params['user_id'] = email_address
206 |     flow.params['state'] = state
207 |     return flow.step1_get_authorize_url(state=state)
208 | 
209 | 
210 | def get_credentials(authorization_code, state):
211 |     """Retrieve credentials using the provided authorization code.
212 | 
213 |     This function exchanges the authorization code for an access token and queries
214 |     the UserInfo API to retrieve the user's e-mail address.
215 |     If a refresh token has been retrieved along with an access token, it is stored
216 |     in the application database using the user's e-mail address as key.
217 |     If no refresh token has been retrieved, the function checks in the application
218 |     database for one and returns it if found or raises a NoRefreshTokenException
219 |     with the authorization URL to redirect the user to.
220 | 
221 |     Args:
222 |     authorization_code: Authorization code to use to retrieve an access token.
223 |     state: State to set to the authorization URL in case of error.
224 |     Returns:
225 |     oauth2client.client.OAuth2Credentials instance containing an access and
226 |     refresh token.
227 |     Raises:
228 |     CodeExchangeError: Could not exchange the authorization code.
229 |     NoRefreshTokenException: No refresh token could be retrieved from the
230 |                                 available sources.
231 |     """
232 |     email_address = ''
233 |     try:
234 |         credentials = exchange_code(authorization_code)
235 |         user_info = get_user_info(credentials)
236 |         import json
237 |         logging.error(f"user_info: {json.dumps(user_info)}")
238 |         email_address = user_info.get('email')
239 |         
240 |         if credentials.refresh_token is not None:
241 |             store_credentials(credentials, user_id=email_address)
242 |             return credentials
243 |         else:
244 |             credentials = get_stored_credentials(user_id=email_address)
245 |             if credentials and credentials.refresh_token is not None:
246 |                 return credentials
247 |     except CodeExchangeException as error:
248 |         logging.error('An error occurred during code exchange.')
249 |         # Drive apps should try to retrieve the user and credentials for the current
250 |         # session.
251 |         # If none is available, redirect the user to the authorization URL.
252 |         error.authorization_url = get_authorization_url(email_address, state)
253 |         raise error
254 |     except NoUserIdException:
255 |         logging.error('No user ID could be retrieved.')
256 |         # No refresh token has been retrieved.
257 |     authorization_url = get_authorization_url(email_address, state)
258 |     raise NoRefreshTokenException(authorization_url)
259 | 
260 | 
```

--------------------------------------------------------------------------------
/src/mcp_gsuite/tools_calendar.py:
--------------------------------------------------------------------------------

```python
  1 | from collections.abc import Sequence
  2 | from mcp.types import (
  3 |     Tool,
  4 |     TextContent,
  5 |     ImageContent,
  6 |     EmbeddedResource,
  7 |     LoggingLevel,
  8 | )
  9 | from . import gauth
 10 | from . import calendar
 11 | import json
 12 | from . import toolhandler
 13 | 
 14 | CALENDAR_ID_ARG="__calendar_id__"
 15 | 
 16 | def get_calendar_id_arg_schema() -> dict[str, str]:
 17 |     return {
 18 |         "type": "string",
 19 |         "description": """Optional ID of the specific agenda for which you are executing this action.
 20 |                           If not provided, the default calendar is being used. 
 21 |                           If not known, the specific calendar id can be retrieved with the list_calendars tool""",
 22 |         "default": "primary"
 23 |     }
 24 | 
 25 | 
 26 | class ListCalendarsToolHandler(toolhandler.ToolHandler):
 27 |     def __init__(self):
 28 |         super().__init__("list_calendars")
 29 | 
 30 |     def get_tool_description(self) -> Tool:
 31 |         return Tool(
 32 |             name=self.name,
 33 |             description="""Lists all calendars accessible by the user. 
 34 |             Call it before any other tool whenever the user specifies a particular agenda (Family, Holidays, etc.).""",
 35 |             inputSchema={
 36 |                 "type": "object",
 37 |                 "properties": {
 38 |                     "__user_id__": self.get_user_id_arg_schema(),
 39 |                 },
 40 |                 "required": [toolhandler.USER_ID_ARG]
 41 |             }
 42 |         )
 43 | 
 44 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
 45 |         user_id = args.get(toolhandler.USER_ID_ARG)
 46 |         if not user_id:
 47 |             raise RuntimeError(f"Missing required argument: {toolhandler.USER_ID_ARG}")
 48 | 
 49 |         calendar_service = calendar.CalendarService(user_id=user_id)
 50 |         calendars = calendar_service.list_calendars()
 51 | 
 52 |         return [
 53 |             TextContent(
 54 |                 type="text",
 55 |                 text=json.dumps(calendars, indent=2)
 56 |             )
 57 |         ]
 58 | 
 59 | class GetCalendarEventsToolHandler(toolhandler.ToolHandler):
 60 |     def __init__(self):
 61 |         super().__init__("get_calendar_events")
 62 | 
 63 |     def get_tool_description(self) -> Tool:
 64 |         return Tool(
 65 |             name=self.name,
 66 |             description="Retrieves calendar events from the user's Google Calendar within a specified time range.",
 67 |             inputSchema={
 68 |                 "type": "object",
 69 |                 "properties": {
 70 |                     "__user_id__": self.get_user_id_arg_schema(),
 71 |                     "__calendar_id__": get_calendar_id_arg_schema(),
 72 |                     "time_min": {
 73 |                         "type": "string",
 74 |                         "description": "Start time in RFC3339 format (e.g. 2024-12-01T00:00:00Z). Defaults to current time if not specified."
 75 |                     },
 76 |                     "time_max": {
 77 |                         "type": "string", 
 78 |                         "description": "End time in RFC3339 format (e.g. 2024-12-31T23:59:59Z). Optional."
 79 |                     },
 80 |                     "max_results": {
 81 |                         "type": "integer",
 82 |                         "description": "Maximum number of events to return (1-2500)",
 83 |                         "minimum": 1,
 84 |                         "maximum": 2500,
 85 |                         "default": 250
 86 |                     },
 87 |                     "show_deleted": {
 88 |                         "type": "boolean",
 89 |                         "description": "Whether to include deleted events",
 90 |                         "default": False
 91 |                     }
 92 |                 },
 93 |                 "required": [toolhandler.USER_ID_ARG]
 94 |             }
 95 |         )
 96 | 
 97 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
 98 |         
 99 |         user_id = args.get(toolhandler.USER_ID_ARG)
100 |         if not user_id:
101 |             raise RuntimeError(f"Missing required argument: {toolhandler.USER_ID_ARG}")
102 |         
103 |         calendar_service = calendar.CalendarService(user_id=user_id)
104 |         events = calendar_service.get_events(
105 |             time_min=args.get('time_min'),
106 |             time_max=args.get('time_max'),
107 |             max_results=args.get('max_results', 250),
108 |             show_deleted=args.get('show_deleted', False),
109 |             calendar_id=args.get(CALENDAR_ID_ARG, 'primary'),
110 |         )
111 | 
112 |         return [
113 |             TextContent(
114 |                 type="text",
115 |                 text=json.dumps(events, indent=2)
116 |             )
117 |         ]
118 | 
119 | class CreateCalendarEventToolHandler(toolhandler.ToolHandler):
120 |     def __init__(self):
121 |         super().__init__("create_calendar_event")
122 | 
123 |     def get_tool_description(self) -> Tool:
124 |         return Tool(
125 |             name=self.name,
126 |             description="Creates a new event in a specified Google Calendar of the specified user.",
127 |             inputSchema={
128 |                 "type": "object",
129 |                 "properties": {
130 |                     "__user_id__": self.get_user_id_arg_schema(),
131 |                     "__calendar_id__": get_calendar_id_arg_schema(),
132 |                     "summary": {
133 |                         "type": "string",
134 |                         "description": "Title of the event"
135 |                     },
136 |                     "location": {
137 |                         "type": "string",
138 |                         "description": "Location of the event (optional)"
139 |                     },
140 |                     "description": {
141 |                         "type": "string",
142 |                         "description": "Description or notes for the event (optional)"
143 |                     },
144 |                     "start_time": {
145 |                         "type": "string",
146 |                         "description": "Start time in RFC3339 format (e.g. 2024-12-01T10:00:00Z)"
147 |                     },
148 |                     "end_time": {
149 |                         "type": "string",
150 |                         "description": "End time in RFC3339 format (e.g. 2024-12-01T11:00:00Z)"
151 |                     },
152 |                     "attendees": {
153 |                         "type": "array",
154 |                         "items": {
155 |                             "type": "string"
156 |                         },
157 |                         "description": "List of attendee email addresses (optional)"
158 |                     },
159 |                     "send_notifications": {
160 |                         "type": "boolean",
161 |                         "description": "Whether to send notifications to attendees",
162 |                         "default": True
163 |                     },
164 |                     "timezone": {
165 |                         "type": "string",
166 |                         "description": "Timezone for the event (e.g. 'America/New_York'). Defaults to UTC if not specified."
167 |                     }
168 |                 },
169 |                 "required": [toolhandler.USER_ID_ARG, "summary", "start_time", "end_time"]
170 |             }
171 |         )
172 | 
173 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
174 |         # Validate required arguments
175 |         required = ["summary", "start_time", "end_time"]
176 |         if not all(key in args for key in required):
177 |             raise RuntimeError(f"Missing required arguments: {', '.join(required)}")
178 | 
179 |         user_id = args.get(toolhandler.USER_ID_ARG)
180 |         if not user_id:
181 |             raise RuntimeError(f"Missing required argument: {toolhandler.USER_ID_ARG}")
182 | 
183 |         calendar_service = calendar.CalendarService(user_id=user_id)
184 |         event = calendar_service.create_event(
185 |             summary=args["summary"],
186 |             start_time=args["start_time"],
187 |             end_time=args["end_time"],
188 |             location=args.get("location"),
189 |             description=args.get("description"),
190 |             attendees=args.get("attendees", []),
191 |             send_notifications=args.get("send_notifications", True),
192 |             timezone=args.get("timezone"),
193 |             calendar_id=args.get(CALENDAR_ID_ARG, 'primary'),
194 |         )
195 | 
196 |         return [
197 |             TextContent(
198 |                 type="text",
199 |                 text=json.dumps(event, indent=2)
200 |             )
201 |         ]
202 |     
203 | class DeleteCalendarEventToolHandler(toolhandler.ToolHandler):
204 |     def __init__(self):
205 |         super().__init__("delete_calendar_event")
206 | 
207 |     def get_tool_description(self) -> Tool:
208 |         return Tool(
209 |             name=self.name,
210 |             description="Deletes an event from the user's Google Calendar by its event ID.",
211 |             inputSchema={
212 |                 "type": "object",
213 |                 "properties": {
214 |                     "__user_id__": self.get_user_id_arg_schema(),
215 |                     "__calendar_id__": get_calendar_id_arg_schema(),
216 |                     "event_id": {
217 |                         "type": "string",
218 |                         "description": "The ID of the calendar event to delete"
219 |                     },
220 |                     "send_notifications": {
221 |                         "type": "boolean",
222 |                         "description": "Whether to send cancellation notifications to attendees",
223 |                         "default": True
224 |                     }
225 |                 },
226 |                 "required": [toolhandler.USER_ID_ARG, "event_id"]
227 |             }
228 |         )
229 | 
230 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
231 |         if "event_id" not in args:
232 |             raise RuntimeError("Missing required argument: event_id")
233 |         
234 |         user_id = args.get(toolhandler.USER_ID_ARG)
235 |         if not user_id:
236 |             raise RuntimeError(f"Missing required argument: {toolhandler.USER_ID_ARG}")
237 | 
238 |         calendar_service = calendar.CalendarService(user_id=user_id)
239 |         success = calendar_service.delete_event(
240 |             event_id=args["event_id"],
241 |             send_notifications=args.get("send_notifications", True),
242 |             calendar_id=args.get(CALENDAR_ID_ARG, 'primary'),
243 |         )
244 | 
245 |         return [
246 |             TextContent(
247 |                 type="text",
248 |                 text=json.dumps({
249 |                     "success": success,
250 |                     "message": "Event successfully deleted" if success else "Failed to delete event"
251 |                 }, indent=2)
252 |             )
253 |         ]
```

--------------------------------------------------------------------------------
/src/mcp_gsuite/gmail.py:
--------------------------------------------------------------------------------

```python
  1 | from googleapiclient.discovery import build 
  2 | from . import gauth
  3 | import logging
  4 | import base64
  5 | import traceback
  6 | from email.mime.text import MIMEText
  7 | from typing import Tuple
  8 | 
  9 | 
 10 | class GmailService():
 11 |     def __init__(self, user_id: str):
 12 |         credentials = gauth.get_stored_credentials(user_id=user_id)
 13 |         if not credentials:
 14 |             raise RuntimeError("No Oauth2 credentials stored")
 15 |         self.service = build('gmail', 'v1', credentials=credentials)
 16 | 
 17 |     def _parse_message(self, txt, parse_body=False) -> dict | None:
 18 |         """
 19 |         Parse a Gmail message into a structured format.
 20 |         
 21 |         Args:
 22 |             txt (dict): Raw message from Gmail API
 23 |             parse_body (bool): Whether to parse and include the message body (default: False)
 24 |         
 25 |         Returns:
 26 |             dict: Parsed message containing comprehensive metadata
 27 |             None: If parsing fails
 28 |         """
 29 |         try:
 30 |             message_id = txt.get('id')
 31 |             thread_id = txt.get('threadId')
 32 |             payload = txt.get('payload', {})
 33 |             headers = payload.get('headers', [])
 34 | 
 35 |             metadata = {
 36 |                 'id': message_id,
 37 |                 'threadId': thread_id,
 38 |                 'historyId': txt.get('historyId'),
 39 |                 'internalDate': txt.get('internalDate'),
 40 |                 'sizeEstimate': txt.get('sizeEstimate'),
 41 |                 'labelIds': txt.get('labelIds', []),
 42 |                 'snippet': txt.get('snippet'),
 43 |             }
 44 | 
 45 |             for header in headers:
 46 |                 name = header.get('name', '').lower()
 47 |                 value = header.get('value', '')
 48 |                 
 49 |                 if name == 'subject':
 50 |                     metadata['subject'] = value
 51 |                 elif name == 'from':
 52 |                     metadata['from'] = value
 53 |                 elif name == 'to':
 54 |                     metadata['to'] = value
 55 |                 elif name == 'date':
 56 |                     metadata['date'] = value
 57 |                 elif name == 'cc':
 58 |                     metadata['cc'] = value
 59 |                 elif name == 'bcc':
 60 |                     metadata['bcc'] = value
 61 |                 elif name == 'message-id':
 62 |                     metadata['message_id'] = value
 63 |                 elif name == 'in-reply-to':
 64 |                     metadata['in_reply_to'] = value
 65 |                 elif name == 'references':
 66 |                     metadata['references'] = value
 67 |                 elif name == 'delivered-to':
 68 |                     metadata['delivered_to'] = value
 69 | 
 70 |             if parse_body:
 71 |                 body = self._extract_body(payload)
 72 |                 if body:
 73 |                     metadata['body'] = body
 74 | 
 75 |                 metadata['mimeType'] = payload.get('mimeType')
 76 | 
 77 |             return metadata
 78 | 
 79 |         except Exception as e:
 80 |             logging.error(f"Error parsing message: {str(e)}")
 81 |             logging.error(traceback.format_exc())
 82 |             return None
 83 | 
 84 |     def _extract_body(self, payload) -> str | None:
 85 |         """
 86 |         Extract the email body from the payload.
 87 |         Handles both multipart and single part messages, including nested multiparts.
 88 |         """
 89 |         try:
 90 |             # For single part text/plain messages
 91 |             if payload.get('mimeType') == 'text/plain':
 92 |                 data = payload.get('body', {}).get('data')
 93 |                 if data:
 94 |                     return base64.urlsafe_b64decode(data).decode('utf-8')
 95 |             
 96 |             # For single part text/html messages
 97 |             if payload.get('mimeType') == 'text/html':
 98 |                 data = payload.get('body', {}).get('data')
 99 |                 if data:
100 |                     return base64.urlsafe_b64decode(data).decode('utf-8')
101 |             
102 |             # For multipart messages (both alternative and related)
103 |             if payload.get('mimeType', '').startswith('multipart/'):
104 |                 parts = payload.get('parts', [])
105 |                 
106 |                 # First try to find a direct text/plain part
107 |                 for part in parts:
108 |                     if part.get('mimeType') == 'text/plain':
109 |                         data = part.get('body', {}).get('data')
110 |                         if data:
111 |                             return base64.urlsafe_b64decode(data).decode('utf-8')
112 |                 
113 |                 # If no direct text/plain, recursively check nested multipart structures
114 |                 for part in parts:
115 |                     if part.get('mimeType', '').startswith('multipart/'):
116 |                         nested_body = self._extract_body(part)
117 |                         if nested_body:
118 |                             return nested_body
119 |                             
120 |                 # If still no body found, try the first part as fallback
121 |                 if parts and 'body' in parts[0] and 'data' in parts[0]['body']:
122 |                     data = parts[0]['body']['data']
123 |                     return base64.urlsafe_b64decode(data).decode('utf-8')
124 | 
125 |             return None
126 | 
127 |         except Exception as e:
128 |             logging.error(f"Error extracting body: {str(e)}")
129 |             return None
130 | 
131 |     def query_emails(self, query=None, max_results=100):
132 |         """
133 |         Query emails from Gmail based on a search query.
134 |         
135 |         Args:
136 |             query (str, optional): Gmail search query (e.g., 'is:unread', 'from:[email protected]')
137 |                                 If None, returns all emails
138 |             max_results (int): Maximum number of emails to retrieve (1-500, default: 100)
139 |         
140 |         Returns:
141 |             list: List of parsed email messages, newest first
142 |         """
143 |         try:
144 |             # Ensure max_results is within API limits
145 |             max_results = min(max(1, max_results), 500)
146 |             
147 |             # Get the list of messages
148 |             result = self.service.users().messages().list(
149 |                 userId='me',
150 |                 maxResults=max_results,
151 |                 q=query if query else ''
152 |             ).execute()
153 | 
154 |             messages = result.get('messages', [])
155 |             parsed = []
156 | 
157 |             # Fetch full message details for each message
158 |             for msg in messages:
159 |                 txt = self.service.users().messages().get(
160 |                     userId='me', 
161 |                     id=msg['id']
162 |                 ).execute()
163 |                 parsed_message = self._parse_message(txt=txt, parse_body=False)
164 |                 if parsed_message:
165 |                     parsed.append(parsed_message)
166 |                     
167 |             return parsed
168 |             
169 |         except Exception as e:
170 |             logging.error(f"Error reading emails: {str(e)}")
171 |             logging.error(traceback.format_exc())
172 |             return []
173 |         
174 |     def get_email_by_id_with_attachments(self, email_id: str) -> Tuple[dict, dict] | Tuple[None, dict]:
175 |         """
176 |         Fetch and parse a complete email message by its ID including attachment IDs.
177 |         
178 |         Args:
179 |             email_id (str): The Gmail message ID to retrieve
180 |         
181 |         Returns:
182 |             Tuple[dict, list]: Complete parsed email message including body and list of attachment IDs
183 |             Tuple[None, list]: If retrieval or parsing fails, returns None for email and empty list for attachment IDs
184 |         """
185 |         try:
186 |             # Fetch the complete message by ID
187 |             message = self.service.users().messages().get(
188 |                 userId='me',
189 |                 id=email_id
190 |             ).execute()
191 |             
192 |             # Parse the message with body included
193 |             parsed_email = self._parse_message(txt=message, parse_body=True)
194 | 
195 |             if parsed_email is None:
196 |                 return None, {}
197 | 
198 |             attachments = {}
199 |             # Check if 'parts' exists in payload before trying to access it
200 |             if "payload" in message and "parts" in message["payload"]:
201 |                 for part in message["payload"]["parts"]:
202 |                     if "body" in part and "attachmentId" in part["body"]:
203 |                         attachment_id = part["body"]["attachmentId"]
204 |                         part_id = part["partId"]
205 |                         attachment = {
206 |                             "filename": part["filename"],
207 |                             "mimeType": part["mimeType"],
208 |                             "attachmentId": attachment_id,
209 |                             "partId": part_id
210 |                         }
211 |                         attachments[part_id] = attachment
212 |             else:
213 |                 # Handle case when there are no parts (single part message)
214 |                 logging.info(f"Email {email_id} does not have 'parts' in payload (likely single part message)")
215 |                 if "payload" in message and "body" in message["payload"] and "attachmentId" in message["payload"]["body"]:
216 |                     # Handle potential attachment in single part message
217 |                     attachment_id = message["payload"]["body"]["attachmentId"]
218 |                     attachment = {
219 |                         "filename": message["payload"].get("filename", "attachment"),
220 |                         "mimeType": message["payload"].get("mimeType", "application/octet-stream"),
221 |                         "attachmentId": attachment_id,
222 |                         "partId": "0"
223 |                     }
224 |                     attachments["0"] = attachment
225 | 
226 |             return parsed_email, attachments
227 |             
228 |         except Exception as e:
229 |             logging.error(f"Error retrieving email {email_id}: {str(e)}")
230 |             logging.error(traceback.format_exc())
231 |             return None, {}
232 |         
233 |     def create_draft(self, to: str, subject: str, body: str, cc: list[str] | None = None) -> dict | None:
234 |         """
235 |         Create a draft email message.
236 |         
237 |         Args:
238 |             to (str): Email address of the recipient
239 |             subject (str): Subject line of the email
240 |             body (str): Body content of the email
241 |             cc (list[str], optional): List of email addresses to CC
242 |             
243 |         Returns:
244 |             dict: Draft message data including the draft ID if successful
245 |             None: If creation fails
246 |         """
247 |         try:
248 |             # Create message body
249 |             message = {
250 |                 'to': to,
251 |                 'subject': subject,
252 |                 'text': body,
253 |             }
254 |             if cc:
255 |                 message['cc'] = ','.join(cc)
256 |                 
257 |             # Create the message in MIME format
258 |             mime_message = MIMEText(body)
259 |             mime_message['to'] = to
260 |             mime_message['subject'] = subject
261 |             if cc:
262 |                 mime_message['cc'] = ','.join(cc)
263 |                 
264 |             # Encode the message
265 |             raw_message = base64.urlsafe_b64encode(mime_message.as_bytes()).decode('utf-8')
266 |             
267 |             # Create the draft
268 |             draft = self.service.users().drafts().create(
269 |                 userId='me',
270 |                 body={
271 |                     'message': {
272 |                         'raw': raw_message
273 |                     }
274 |                 }
275 |             ).execute()
276 |             
277 |             return draft
278 |             
279 |         except Exception as e:
280 |             logging.error(f"Error creating draft: {str(e)}")
281 |             logging.error(traceback.format_exc())
282 |             return None
283 |         
284 |     def delete_draft(self, draft_id: str) -> bool:
285 |         """
286 |         Delete a draft email message.
287 |         
288 |         Args:
289 |             draft_id (str): The ID of the draft to delete
290 |             
291 |         Returns:
292 |             bool: True if deletion was successful, False otherwise
293 |         """
294 |         try:
295 |             self.service.users().drafts().delete(
296 |                 userId='me',
297 |                 id=draft_id
298 |             ).execute()
299 |             return True
300 |             
301 |         except Exception as e:
302 |             logging.error(f"Error deleting draft {draft_id}: {str(e)}")
303 |             logging.error(traceback.format_exc())
304 |             return False
305 |         
306 |     def create_reply(self, original_message: dict, reply_body: str, send: bool = False, cc: list[str] | None = None) -> dict | None:
307 |         """
308 |         Create a reply to an email message and either send it or save as draft.
309 |         
310 |         Args:
311 |             original_message (dict): The original message data (as returned by get_email_by_id)
312 |             reply_body (str): Body content of the reply
313 |             send (bool): If True, sends the reply immediately. If False, saves as draft.
314 |             cc (list[str], optional): List of email addresses to CC
315 |             
316 |         Returns:
317 |             dict: Sent message or draft data if successful
318 |             None: If operation fails
319 |         """
320 |         try:
321 |             to_address = original_message.get('from')
322 |             if not to_address:
323 |                 raise ValueError("Could not determine original sender's address")
324 |             
325 |             subject = original_message.get('subject', '')
326 |             if not subject.lower().startswith('re:'):
327 |                 subject = f"Re: {subject}"
328 | 
329 | 
330 |             original_date = original_message.get('date', '')
331 |             original_from = original_message.get('from', '')
332 |             original_body = original_message.get('body', '')
333 |         
334 |             full_reply_body = (
335 |                 f"{reply_body}\n\n"
336 |                 f"On {original_date}, {original_from} wrote:\n"
337 |                 f"> {original_body.replace('\n', '\n> ') if original_body else '[No message body]'}"
338 |             )
339 | 
340 |             mime_message = MIMEText(full_reply_body)
341 |             mime_message['to'] = to_address
342 |             mime_message['subject'] = subject
343 |             if cc:
344 |                 mime_message['cc'] = ','.join(cc)
345 |                 
346 |             mime_message['In-Reply-To'] = original_message.get('id', '')
347 |             mime_message['References'] = original_message.get('id', '')
348 |             
349 |             raw_message = base64.urlsafe_b64encode(mime_message.as_bytes()).decode('utf-8')
350 |             
351 |             message_body = {
352 |                 'raw': raw_message,
353 |                 'threadId': original_message.get('threadId')  # Ensure it's added to the same thread
354 |             }
355 | 
356 |             if send:
357 |                 # Send the reply immediately
358 |                 result = self.service.users().messages().send(
359 |                     userId='me',
360 |                     body=message_body
361 |                 ).execute()
362 |             else:
363 |                 # Save as draft
364 |                 result = self.service.users().drafts().create(
365 |                     userId='me',
366 |                     body={
367 |                         'message': message_body
368 |                     }
369 |                 ).execute()
370 |             
371 |             return result
372 |             
373 |         except Exception as e:
374 |             logging.error(f"Error {'sending' if send else 'drafting'} reply: {str(e)}")
375 |             logging.error(traceback.format_exc())
376 |             return None
377 |         
378 |     def get_attachment(self, message_id: str, attachment_id: str) -> dict | None:
379 |         """
380 |         Retrieves a Gmail attachment by its ID.
381 |         
382 |         Args:
383 |             message_id (str): The ID of the Gmail message containing the attachment
384 |             attachment_id (str): The ID of the attachment to retrieve
385 |         
386 |         Returns:
387 |             dict: Attachment data including filename and base64-encoded content
388 |             None: If retrieval fails
389 |         """
390 |         try:
391 |             attachment = self.service.users().messages().attachments().get(
392 |                 userId='me',
393 |                 messageId=message_id, 
394 |                 id=attachment_id
395 |             ).execute()
396 |             return {
397 |                 "size": attachment.get("size"),
398 |                 "data": attachment.get("data")
399 |             }
400 |             
401 |         except Exception as e:
402 |             logging.error(f"Error retrieving attachment {attachment_id} from message {message_id}: {str(e)}")
403 |             logging.error(traceback.format_exc())
404 |             return None
```

--------------------------------------------------------------------------------
/src/mcp_gsuite/tools_gmail.py:
--------------------------------------------------------------------------------

```python
  1 | from collections.abc import Sequence
  2 | from mcp.types import (
  3 |     Tool,
  4 |     TextContent,
  5 |     ImageContent,
  6 |     EmbeddedResource,
  7 |     LoggingLevel,
  8 | )
  9 | from . import gmail
 10 | import json
 11 | from . import toolhandler
 12 | import base64
 13 | 
 14 | def decode_base64_data(file_data):
 15 |     standard_base64_data = file_data.replace("-", "+").replace("_", "/")
 16 |     missing_padding = len(standard_base64_data) % 4
 17 |     if missing_padding:
 18 |         standard_base64_data += '=' * (4 - missing_padding)
 19 |     return base64.b64decode(standard_base64_data, validate=True)
 20 | 
 21 | class QueryEmailsToolHandler(toolhandler.ToolHandler):
 22 |     def __init__(self):
 23 |         super().__init__("query_gmail_emails")
 24 | 
 25 |     def get_tool_description(self) -> Tool:
 26 |         return Tool(
 27 |             name=self.name,
 28 |             description="""Query Gmail emails based on an optional search query. 
 29 |             Returns emails in reverse chronological order (newest first).
 30 |             Returns metadata such as subject and also a short summary of the content.
 31 |             """,
 32 |             inputSchema={
 33 |                 "type": "object",
 34 |                 "properties": {
 35 |                     "__user_id__": self.get_user_id_arg_schema(),
 36 |                     "query": {
 37 |                         "type": "string",
 38 |                         "description": """Gmail search query (optional). Examples:
 39 |                             - a $string: Search email body, subject, and sender information for $string
 40 |                             - 'is:unread' for unread emails
 41 |                             - 'from:[email protected]' for emails from a specific sender
 42 |                             - 'newer_than:2d' for emails from last 2 days
 43 |                             - 'has:attachment' for emails with attachments
 44 |                             If not provided, returns recent emails without filtering.""",
 45 |                         "required": False
 46 |                     },
 47 |                     "max_results": {
 48 |                         "type": "integer",
 49 |                         "description": "Maximum number of emails to retrieve (1-500)",
 50 |                         "minimum": 1,
 51 |                         "maximum": 500,
 52 |                         "default": 100
 53 |                     }
 54 |                 },
 55 |                 "required": [toolhandler.USER_ID_ARG]
 56 |             }
 57 |         )
 58 | 
 59 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
 60 | 
 61 |         user_id = args.get(toolhandler.USER_ID_ARG)
 62 |         if not user_id:
 63 |             raise RuntimeError(f"Missing required argument: {toolhandler.USER_ID_ARG}")
 64 | 
 65 |         gmail_service = gmail.GmailService(user_id=user_id)
 66 |         query = args.get('query')
 67 |         max_results = args.get('max_results', 100)
 68 |         emails = gmail_service.query_emails(query=query, max_results=max_results)
 69 | 
 70 |         return [
 71 |             TextContent(
 72 |                 type="text",
 73 |                 text=json.dumps(emails, indent=2)
 74 |             )
 75 |         ]
 76 | 
 77 | class GetEmailByIdToolHandler(toolhandler.ToolHandler):
 78 |     def __init__(self):
 79 |         super().__init__("get_gmail_email")
 80 | 
 81 |     def get_tool_description(self) -> Tool:
 82 |         return Tool(
 83 |             name=self.name,
 84 |             description="Retrieves a complete Gmail email message by its ID, including the full message body and attachment IDs.",
 85 |             inputSchema={
 86 |                 "type": "object",
 87 |                 "properties": {
 88 |                     "__user_id__": self.get_user_id_arg_schema(),
 89 |                     "email_id": {
 90 |                         "type": "string",
 91 |                         "description": "The ID of the Gmail message to retrieve"
 92 |                     }
 93 |                 },
 94 |                 "required": ["email_id", toolhandler.USER_ID_ARG]
 95 |             }
 96 |         )
 97 | 
 98 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
 99 |         if "email_id" not in args:
100 |             raise RuntimeError("Missing required argument: email_id")
101 | 
102 |         user_id = args.get(toolhandler.USER_ID_ARG)
103 |         if not user_id:
104 |             raise RuntimeError(f"Missing required argument: {toolhandler.USER_ID_ARG}")
105 |         gmail_service = gmail.GmailService(user_id=user_id)
106 |         email, attachments = gmail_service.get_email_by_id_with_attachments(args["email_id"])
107 | 
108 |         if email is None:
109 |             return [
110 |                 TextContent(
111 |                     type="text",
112 |                     text=f"Failed to retrieve email with ID: {args['email_id']}"
113 |                 )
114 |             ]
115 | 
116 |         email["attachments"] = attachments
117 | 
118 |         return [
119 |             TextContent(
120 |                 type="text",
121 |                 text=json.dumps(email, indent=2)
122 |             )
123 |         ]
124 | 
125 | class BulkGetEmailsByIdsToolHandler(toolhandler.ToolHandler):
126 |     def __init__(self):
127 |         super().__init__("bulk_get_gmail_emails")
128 | 
129 |     def get_tool_description(self) -> Tool:
130 |         return Tool(
131 |             name=self.name,
132 |             description="Retrieves multiple Gmail email messages by their IDs in a single request, including the full message bodies and attachment IDs.",
133 |             inputSchema={
134 |                 "type": "object",
135 |                 "properties": {
136 |                     "__user_id__": self.get_user_id_arg_schema(),
137 |                     "email_ids": {
138 |                         "type": "array",
139 |                         "items": {
140 |                             "type": "string"
141 |                         },
142 |                         "description": "List of Gmail message IDs to retrieve"
143 |                     }
144 |                 },
145 |                 "required": ["email_ids", toolhandler.USER_ID_ARG]
146 |             }
147 |         )
148 | 
149 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
150 |         if "email_ids" not in args:
151 |             raise RuntimeError("Missing required argument: email_ids")
152 | 
153 |         user_id = args.get(toolhandler.USER_ID_ARG)
154 |         if not user_id:
155 |             raise RuntimeError(f"Missing required argument: {toolhandler.USER_ID_ARG}")
156 |         gmail_service = gmail.GmailService(user_id=user_id)
157 |         
158 |         results = []
159 |         for email_id in args["email_ids"]:
160 |             email, attachments = gmail_service.get_email_by_id_with_attachments(email_id)
161 |             if email is not None:
162 |                 email["attachments"] = attachments
163 |                 results.append(email)
164 | 
165 |         if not results:
166 |             return [
167 |                 TextContent(
168 |                     type="text",
169 |                     text=f"Failed to retrieve any emails from the provided IDs"
170 |                 )
171 |             ]
172 | 
173 |         return [
174 |             TextContent(
175 |                 type="text",
176 |                 text=json.dumps(results, indent=2)
177 |             )
178 |         ]
179 | 
180 | class CreateDraftToolHandler(toolhandler.ToolHandler):
181 |     def __init__(self):
182 |         super().__init__("create_gmail_draft")
183 | 
184 |     def get_tool_description(self) -> Tool:
185 |         return Tool(
186 |             name=self.name,
187 |             description="""Creates a draft email message from scratch in Gmail with specified recipient, subject, body, and optional CC recipients.
188 |             
189 |             Do NOT use this tool when you want to draft or send a REPLY to an existing message. This tool does NOT include any previous message content. Use the reply_gmail_email tool
190 |             with send=False instead."
191 |             """,
192 |             inputSchema={
193 |                 "type": "object",
194 |                 "properties": {
195 |                     "__user_id__": self.get_user_id_arg_schema(),
196 |                     "to": {
197 |                         "type": "string",
198 |                         "description": "Email address of the recipient"
199 |                     },
200 |                     "subject": {
201 |                         "type": "string",
202 |                         "description": "Subject line of the email"
203 |                     },
204 |                     "body": {
205 |                         "type": "string",
206 |                         "description": "Body content of the email"
207 |                     },
208 |                     "cc": {
209 |                         "type": "array",
210 |                         "items": {
211 |                             "type": "string"
212 |                         },
213 |                         "description": "Optional list of email addresses to CC"
214 |                     }
215 |                 },
216 |                 "required": ["to", "subject", "body", toolhandler.USER_ID_ARG]
217 |             }
218 |         )
219 | 
220 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
221 |         required = ["to", "subject", "body"]
222 |         if not all(key in args for key in required):
223 |             raise RuntimeError(f"Missing required arguments: {', '.join(required)}")
224 | 
225 |         user_id = args.get(toolhandler.USER_ID_ARG)
226 |         if not user_id:
227 |             raise RuntimeError(f"Missing required argument: {toolhandler.USER_ID_ARG}")
228 |         gmail_service = gmail.GmailService(user_id=user_id)
229 |         draft = gmail_service.create_draft(
230 |             to=args["to"],
231 |             subject=args["subject"],
232 |             body=args["body"],
233 |             cc=args.get("cc")
234 |         )
235 | 
236 |         if draft is None:
237 |             return [
238 |                 TextContent(
239 |                     type="text",
240 |                     text="Failed to create draft email"
241 |                 )
242 |             ]
243 | 
244 |         return [
245 |             TextContent(
246 |                 type="text",
247 |                 text=json.dumps(draft, indent=2)
248 |             )
249 |         ]
250 | 
251 | class DeleteDraftToolHandler(toolhandler.ToolHandler):
252 |     def __init__(self):
253 |         super().__init__("delete_gmail_draft")
254 | 
255 |     def get_tool_description(self) -> Tool:
256 |         return Tool(
257 |             name=self.name,
258 |             description="Deletes a Gmail draft message by its ID. This action cannot be undone.",
259 |             inputSchema={
260 |                 "type": "object",
261 |                 "properties": {
262 |                     "__user_id__": self.get_user_id_arg_schema(),
263 |                     "draft_id": {
264 |                         "type": "string",
265 |                         "description": "The ID of the draft to delete"
266 |                     }
267 |                 },
268 |                 "required": ["draft_id", toolhandler.USER_ID_ARG]
269 |             }
270 |         )
271 | 
272 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
273 |         if "draft_id" not in args:
274 |             raise RuntimeError("Missing required argument: draft_id")
275 | 
276 |         user_id = args.get(toolhandler.USER_ID_ARG)
277 |         if not user_id:
278 |             raise RuntimeError(f"Missing required argument: {toolhandler.USER_ID_ARG}")
279 |         gmail_service = gmail.GmailService(user_id=user_id)
280 |         success = gmail_service.delete_draft(args["draft_id"])
281 | 
282 |         return [
283 |             TextContent(
284 |                 type="text",
285 |                 text="Successfully deleted draft" if success else f"Failed to delete draft with ID: {args['draft_id']}"
286 |             )
287 |         ]
288 | 
289 | class ReplyEmailToolHandler(toolhandler.ToolHandler):
290 |     def __init__(self):
291 |         super().__init__("reply_gmail_email")
292 | 
293 |     def get_tool_description(self) -> Tool:
294 |         return Tool(
295 |             name=self.name,
296 |             description="""Creates a reply to an existing Gmail email message and either sends it or saves as draft.
297 | 
298 |             Use this tool if you want to draft a reply. Use the 'cc' argument if you want to perform a "reply all".
299 |             """,
300 |             inputSchema={
301 |                 "type": "object",
302 |                 "properties": {
303 |                     "__user_id__": self.get_user_id_arg_schema(),
304 |                     "original_message_id": {
305 |                         "type": "string",
306 |                         "description": "The ID of the Gmail message to reply to"
307 |                     },
308 |                     "reply_body": {
309 |                         "type": "string",
310 |                         "description": "The body content of your reply message"
311 |                     },
312 |                     "send": {
313 |                         "type": "boolean",
314 |                         "description": "If true, sends the reply immediately. If false, saves as draft.",
315 |                         "default": False
316 |                     },
317 |                     "cc": {
318 |                         "type": "array",
319 |                         "items": {
320 |                             "type": "string"
321 |                         },
322 |                         "description": "Optional list of email addresses to CC on the reply"
323 |                     }
324 |                 },
325 |                 "required": ["original_message_id", "reply_body", toolhandler.USER_ID_ARG]
326 |             }
327 |         )
328 | 
329 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
330 |         if not all(key in args for key in ["original_message_id", "reply_body"]):
331 |             raise RuntimeError("Missing required arguments: original_message_id and reply_body")
332 | 
333 |         user_id = args.get(toolhandler.USER_ID_ARG)
334 |         if not user_id:
335 |             raise RuntimeError(f"Missing required argument: {toolhandler.USER_ID_ARG}")
336 |         gmail_service = gmail.GmailService(user_id=user_id)
337 |         
338 |         # First get the original message to extract necessary information
339 |         original_message = gmail_service.get_email_by_id(args["original_message_id"])
340 |         if original_message is None:
341 |             return [
342 |                 TextContent(
343 |                     type="text",
344 |                     text=f"Failed to retrieve original message with ID: {args['original_message_id']}"
345 |                 )
346 |             ]
347 | 
348 |         # Create and send/draft the reply
349 |         result = gmail_service.create_reply(
350 |             original_message=original_message,
351 |             reply_body=args.get("reply_body", ""),
352 |             send=args.get("send", False),
353 |             cc=args.get("cc")
354 |         )
355 | 
356 |         if result is None:
357 |             return [
358 |                 TextContent(
359 |                     type="text",
360 |                     text=f"Failed to {'send' if args.get('send', True) else 'draft'} reply email"
361 |                 )
362 |             ]
363 | 
364 |         return [
365 |             TextContent(
366 |                 type="text",
367 |                 text=json.dumps(result, indent=2)
368 |             )
369 |         ]
370 | 
371 | class GetAttachmentToolHandler(toolhandler.ToolHandler):
372 |     def __init__(self):
373 |         super().__init__("get_gmail_attachment")
374 | 
375 |     def get_tool_description(self) -> Tool:
376 |         return Tool(
377 |             name=self.name,
378 |             description="Retrieves a Gmail attachment by its ID.",
379 |             inputSchema={
380 |                 "type": "object",
381 |                 "properties": {
382 |                     "__user_id__": self.get_user_id_arg_schema(),
383 |                     "message_id": {
384 |                         "type": "string",
385 |                         "description": "The ID of the Gmail message containing the attachment"
386 |                     },
387 |                     "attachment_id": {
388 |                         "type": "string",
389 |                         "description": "The ID of the attachment to retrieve"
390 |                     },
391 |                     "mime_type": {
392 |                         "type": "string",
393 |                         "description": "The MIME type of the attachment"
394 |                     },
395 |                     "filename": {
396 |                         "type": "string",
397 |                         "description": "The filename of the attachment"
398 |                     },
399 |                     "save_to_disk": {
400 |                         "type": "string",
401 |                         "description": "The fullpath to save the attachment to disk. If not provided, the attachment is returned as a resource."
402 |                     }
403 |                 },
404 |                 "required": ["message_id", "attachment_id", "mime_type", "filename", toolhandler.USER_ID_ARG]
405 |             }
406 |         )
407 | 
408 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
409 |         if "message_id" not in args:
410 |             raise RuntimeError("Missing required argument: message_id")
411 |         if "attachment_id" not in args:
412 |             raise RuntimeError("Missing required argument: attachment_id")
413 |         if "mime_type" not in args:
414 |             raise RuntimeError("Missing required argument: mime_type")
415 |         if "filename" not in args:
416 |             raise RuntimeError("Missing required argument: filename")
417 |         filename = args["filename"]
418 |         mime_type = args["mime_type"]
419 |         user_id = args.get(toolhandler.USER_ID_ARG)
420 |         if not user_id:
421 |             raise RuntimeError(f"Missing required argument: {toolhandler.USER_ID_ARG}")
422 |         gmail_service = gmail.GmailService(user_id=user_id)
423 |         attachment_data = gmail_service.get_attachment(args["message_id"], args["attachment_id"])
424 | 
425 |         if attachment_data is None:
426 |             return [
427 |                 TextContent(
428 |                     type="text",
429 |                     text=f"Failed to retrieve attachment with ID: {args['attachment_id']} from message: {args['message_id']}"
430 |                 )
431 |             ]
432 | 
433 |         file_data = attachment_data["data"]
434 |         attachment_url = f"attachment://gmail/{args['message_id']}/{args['attachment_id']}/{filename}"
435 |         if args.get("save_to_disk"):
436 |             decoded_data = decode_base64_data(file_data)
437 |             with open(args["save_to_disk"], "wb") as f:
438 |                 f.write(decoded_data)
439 |             return [
440 |                 TextContent(
441 |                     type="text",
442 |                     text=f"Attachment saved to disk: {args['save_to_disk']}"
443 |                 )
444 |             ]
445 |         return [
446 |             EmbeddedResource(
447 |                 type="resource",
448 |                 resource={
449 |                     "blob": file_data,
450 |                     "uri": attachment_url,
451 |                     "mimeType": mime_type,
452 |                 },
453 |             )
454 |         ]
455 | 
456 | class BulkSaveAttachmentsToolHandler(toolhandler.ToolHandler):
457 |     def __init__(self):
458 |         super().__init__("bulk_save_gmail_attachments")
459 | 
460 |     def get_tool_description(self) -> Tool:
461 |         return Tool(
462 |             name=self.name,
463 |             description="Saves multiple Gmail attachments to disk by their message IDs and attachment IDs in a single request.",
464 |             inputSchema={
465 |                 "type": "object",
466 |                 "properties": {
467 |                     "__user_id__": self.get_user_id_arg_schema(),
468 |                     "attachments": {
469 |                         "type": "array",
470 |                         "items": {
471 |                             "type": "object",
472 |                             "properties": {
473 |                                 "message_id": {
474 |                                     "type": "string",
475 |                                     "description": "ID of the Gmail message containing the attachment"
476 |                                 },
477 |                                 "part_id": {
478 |                                     "type": "string", 
479 |                                     "description": "ID of the part containing the attachment"
480 |                                 },
481 |                                 "save_path": {
482 |                                     "type": "string",
483 |                                     "description": "Path where the attachment should be saved"
484 |                                 }
485 |                             },
486 |                             "required": ["message_id", "part_id", "save_path"]
487 |                         }
488 |                     }
489 |                 },
490 |                 "required": ["attachments", toolhandler.USER_ID_ARG]
491 |             }
492 |         )
493 | 
494 |     def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
495 |         if "attachments" not in args:
496 |             raise RuntimeError("Missing required argument: attachments")
497 | 
498 |         user_id = args.get(toolhandler.USER_ID_ARG)
499 |         if not user_id:
500 |             raise RuntimeError(f"Missing required argument: {toolhandler.USER_ID_ARG}")
501 | 
502 |         gmail_service = gmail.GmailService(user_id=user_id)
503 |         results = []
504 | 
505 |         for attachment_info in args["attachments"]:
506 |             # get attachment data from message_id and part_id
507 |             message, attachments = gmail_service.get_email_by_id_with_attachments(
508 |                 attachment_info["message_id"]
509 |             )
510 |             if message is None:
511 |                 results.append(
512 |                     TextContent(
513 |                         type="text",
514 |                         text=f"Failed to retrieve message with ID: {attachment_info['message_id']}"
515 |                     )
516 |                 )
517 |                 continue
518 |             # get attachment_id from part_id
519 |             attachment_id = attachments[attachment_info["part_id"]]["attachmentId"]
520 |             attachment_data = gmail_service.get_attachment(
521 |                 attachment_info["message_id"], 
522 |                 attachment_id
523 |             )
524 |             if attachment_data is None:
525 |                 results.append(
526 |                     TextContent(
527 |                         type="text",
528 |                         text=f"Failed to retrieve attachment with ID: {attachment_id} from message: {attachment_info['message_id']}"
529 |                     )
530 |                 )
531 |                 continue
532 | 
533 |             file_data = attachment_data["data"]
534 |             try:    
535 |                 decoded_data = decode_base64_data(file_data)
536 |                 with open(attachment_info["save_path"], "wb") as f:
537 |                     f.write(decoded_data)
538 |                 results.append(
539 |                     TextContent(
540 |                         type="text",
541 |                         text=f"Attachment saved to: {attachment_info['save_path']}"
542 |                     )
543 |                 )
544 |             except Exception as e:
545 |                 results.append(
546 |                     TextContent(
547 |                         type="text",
548 |                         text=f"Failed to save attachment to {attachment_info['save_path']}: {str(e)}"
549 |                     )
550 |                 )
551 |                 continue
552 | 
553 |         return results
554 | 
```

--------------------------------------------------------------------------------
/google-calendar-api-openapi-spec.yaml:
--------------------------------------------------------------------------------

```yaml
   1 | swagger: "2.0"
   2 | info:
   3 |   title: Calendar
   4 |   description: Manipulates events and other calendar data.
   5 |   contact:
   6 |     name: Google
   7 |     url: https://google.com
   8 |   version: v3
   9 | host: www.googleapis.com
  10 | basePath: /calendar/v3
  11 | schemes:
  12 | - http
  13 | produces:
  14 | - application/json
  15 | consumes:
  16 | - application/json
  17 | paths:
  18 |   /calendars:
  19 |     post:
  20 |       summary: Create Calendar
  21 |       description: Creates a secondary calendar
  22 |       operationId: calendar.calendars.insert
  23 |       parameters:
  24 |       - in: body
  25 |         name: body
  26 |         schema:
  27 |           $ref: '#/definitions/holder'
  28 |       responses:
  29 |         200:
  30 |           description: OK
  31 |       tags:
  32 |       - Calendar
  33 |   /calendars/{calendarId}:
  34 |     delete:
  35 |       summary: CreaDeletete Calendar
  36 |       description: Deletes a secondary calendar
  37 |       operationId: calendar.calendars.delete
  38 |       parameters:
  39 |       - in: path
  40 |         name: calendarId
  41 |         description: Calendar identifier
  42 |       responses:
  43 |         200:
  44 |           description: OK
  45 |       tags:
  46 |       - Calendar
  47 |     get:
  48 |       summary: Get Calendar
  49 |       description: Returns metadata for a calendar
  50 |       operationId: calendar.calendars.get
  51 |       parameters:
  52 |       - in: path
  53 |         name: calendarId
  54 |         description: Calendar identifier
  55 |       responses:
  56 |         200:
  57 |           description: OK
  58 |       tags:
  59 |       - Calendar
  60 |     patch:
  61 |       summary: Update Calendar
  62 |       description: Updates metadata for a calendar
  63 |       operationId: calendar.calendars.patch
  64 |       parameters:
  65 |       - in: body
  66 |         name: body
  67 |         schema:
  68 |           $ref: '#/definitions/holder'
  69 |       - in: path
  70 |         name: calendarId
  71 |         description: Calendar identifier
  72 |       responses:
  73 |         200:
  74 |           description: OK
  75 |       tags:
  76 |       - Calendar
  77 |     put:
  78 |       summary: Update Calendar
  79 |       description: Updates metadata for a calendar
  80 |       operationId: calendar.calendars.update
  81 |       parameters:
  82 |       - in: body
  83 |         name: body
  84 |         schema:
  85 |           $ref: '#/definitions/holder'
  86 |       - in: path
  87 |         name: calendarId
  88 |         description: Calendar identifier
  89 |       responses:
  90 |         200:
  91 |           description: OK
  92 |       tags:
  93 |       - Calendar
  94 |   /calendars/{calendarId}/acl:
  95 |     get:
  96 |       summary: Get Calendar ACL
  97 |       description: Returns the rules in the access control list for the calendar
  98 |       operationId: calendar.acl.list
  99 |       parameters:
 100 |       - in: path
 101 |         name: calendarId
 102 |         description: Calendar identifier
 103 |       - in: query
 104 |         name: maxResults
 105 |         description: Maximum number of entries returned on one result page
 106 |       - in: query
 107 |         name: pageToken
 108 |         description: Token specifying which result page to return
 109 |       - in: query
 110 |         name: showDeleted
 111 |         description: Whether to include deleted ACLs in the result
 112 |       - in: query
 113 |         name: syncToken
 114 |         description: Token obtained from the nextSyncToken field returned on the last
 115 |           page of results from the previous list request
 116 |       responses:
 117 |         200:
 118 |           description: OK
 119 |       tags:
 120 |       - Calendar ACL
 121 |     post:
 122 |       summary: Create Calendar ACL
 123 |       description: Creates an access control rule
 124 |       operationId: calendar.acl.insert
 125 |       parameters:
 126 |       - in: body
 127 |         name: body
 128 |         schema:
 129 |           $ref: '#/definitions/holder'
 130 |       - in: path
 131 |         name: calendarId
 132 |         description: Calendar identifier
 133 |       responses:
 134 |         200:
 135 |           description: OK
 136 |       tags:
 137 |       - Calendar ACL
 138 |   /calendars/{calendarId}/acl/watch:
 139 |     post:
 140 |       summary: Watch Calendar ACL
 141 |       description: Watch for changes to ACL resources
 142 |       operationId: calendar.acl.watch
 143 |       parameters:
 144 |       - in: path
 145 |         name: calendarId
 146 |         description: Calendar identifier
 147 |       - in: query
 148 |         name: maxResults
 149 |         description: Maximum number of entries returned on one result page
 150 |       - in: query
 151 |         name: pageToken
 152 |         description: Token specifying which result page to return
 153 |       - in: body
 154 |         name: resource
 155 |         schema:
 156 |           $ref: '#/definitions/holder'
 157 |       - in: query
 158 |         name: showDeleted
 159 |         description: Whether to include deleted ACLs in the result
 160 |       - in: query
 161 |         name: syncToken
 162 |         description: Token obtained from the nextSyncToken field returned on the last
 163 |           page of results from the previous list request
 164 |       responses:
 165 |         200:
 166 |           description: OK
 167 |       tags:
 168 |       - Calendar ACL
 169 |   /calendars/{calendarId}/acl/{ruleId}:
 170 |     delete:
 171 |       summary: Delete Calendar ACL
 172 |       description: Deletes an access control rule
 173 |       operationId: calendar.acl.delete
 174 |       parameters:
 175 |       - in: path
 176 |         name: calendarId
 177 |         description: Calendar identifier
 178 |       - in: path
 179 |         name: ruleId
 180 |         description: ACL rule identifier
 181 |       responses:
 182 |         200:
 183 |           description: OK
 184 |       tags:
 185 |       - Calendar ACL
 186 |     get:
 187 |       summary: Get Calendar ACL
 188 |       description: Returns an access control rule
 189 |       operationId: calendar.acl.get
 190 |       parameters:
 191 |       - in: path
 192 |         name: calendarId
 193 |         description: Calendar identifier
 194 |       - in: path
 195 |         name: ruleId
 196 |         description: ACL rule identifier
 197 |       responses:
 198 |         200:
 199 |           description: OK
 200 |       tags:
 201 |       - Calendar ACL
 202 |     patch:
 203 |       summary: Update Calendar ACL
 204 |       description: Updates an access control rule
 205 |       operationId: calendar.acl.patch
 206 |       parameters:
 207 |       - in: body
 208 |         name: body
 209 |         schema:
 210 |           $ref: '#/definitions/holder'
 211 |       - in: path
 212 |         name: calendarId
 213 |         description: Calendar identifier
 214 |       - in: path
 215 |         name: ruleId
 216 |         description: ACL rule identifier
 217 |       responses:
 218 |         200:
 219 |           description: OK
 220 |       tags:
 221 |       - Calendar ACL
 222 |     put:
 223 |       summary: Update Calendar ACL
 224 |       description: Updates an access control rule
 225 |       operationId: calendar.acl.update
 226 |       parameters:
 227 |       - in: body
 228 |         name: body
 229 |         schema:
 230 |           $ref: '#/definitions/holder'
 231 |       - in: path
 232 |         name: calendarId
 233 |         description: Calendar identifier
 234 |       - in: path
 235 |         name: ruleId
 236 |         description: ACL rule identifier
 237 |       responses:
 238 |         200:
 239 |           description: OK
 240 |       tags:
 241 |       - Calendar ACL
 242 |   /calendars/{calendarId}/clear:
 243 |     post:
 244 |       summary: Clear Primary Calendar
 245 |       description: Clears a primary calendar
 246 |       operationId: calendar.calendars.clear
 247 |       parameters:
 248 |       - in: path
 249 |         name: calendarId
 250 |         description: Calendar identifier
 251 |       responses:
 252 |         200:
 253 |           description: OK
 254 |       tags:
 255 |       - Calendar
 256 |   /calendars/{calendarId}/events:
 257 |     get:
 258 |       summary: Get Events
 259 |       description: Returns events on the specified calendar
 260 |       operationId: calendar.events.list
 261 |       parameters:
 262 |       - in: query
 263 |         name: alwaysIncludeEmail
 264 |         description: Whether to always include a value in the email field for the
 265 |           organizer, creator and attendees, even if no real email is available (i
 266 |       - in: path
 267 |         name: calendarId
 268 |         description: Calendar identifier
 269 |       - in: query
 270 |         name: iCalUID
 271 |         description: Specifies event ID in the iCalendar format to be included in
 272 |           the response
 273 |       - in: query
 274 |         name: maxAttendees
 275 |         description: The maximum number of attendees to include in the response
 276 |       - in: query
 277 |         name: maxResults
 278 |         description: Maximum number of events returned on one result page
 279 |       - in: query
 280 |         name: orderBy
 281 |         description: The order of the events returned in the result
 282 |       - in: query
 283 |         name: pageToken
 284 |         description: Token specifying which result page to return
 285 |       - in: query
 286 |         name: privateExtendedProperty
 287 |         description: Extended properties constraint specified as propertyName=value
 288 |       - in: query
 289 |         name: q
 290 |         description: Free text search terms to find events that match these terms
 291 |           in any field, except for extended properties
 292 |       - in: query
 293 |         name: sharedExtendedProperty
 294 |         description: Extended properties constraint specified as propertyName=value
 295 |       - in: query
 296 |         name: showDeleted
 297 |         description: Whether to include deleted events (with status equals "cancelled")
 298 |           in the result
 299 |       - in: query
 300 |         name: showHiddenInvitations
 301 |         description: Whether to include hidden invitations in the result
 302 |       - in: query
 303 |         name: singleEvents
 304 |         description: Whether to expand recurring events into instances and only return
 305 |           single one-off events and instances of recurring events, but not the underlying
 306 |           recurring events themselves
 307 |       - in: query
 308 |         name: syncToken
 309 |         description: Token obtained from the nextSyncToken field returned on the last
 310 |           page of results from the previous list request
 311 |       - in: query
 312 |         name: timeMax
 313 |         description: Upper bound (exclusive) for an event's start time to filter by
 314 |       - in: query
 315 |         name: timeMin
 316 |         description: Lower bound (inclusive) for an event's end time to filter by
 317 |       - in: query
 318 |         name: timeZone
 319 |         description: Time zone used in the response
 320 |       - in: query
 321 |         name: updatedMin
 322 |         description: Lower bound for an event's last modification time (as a RFC3339
 323 |           timestamp) to filter by
 324 |       responses:
 325 |         200:
 326 |           description: OK
 327 |       tags:
 328 |       - Event
 329 |     post:
 330 |       summary: Create Event
 331 |       description: Creates an event
 332 |       operationId: calendar.events.insert
 333 |       parameters:
 334 |       - in: body
 335 |         name: body
 336 |         schema:
 337 |           $ref: '#/definitions/holder'
 338 |       - in: path
 339 |         name: calendarId
 340 |         description: Calendar identifier
 341 |       - in: query
 342 |         name: maxAttendees
 343 |         description: The maximum number of attendees to include in the response
 344 |       - in: query
 345 |         name: sendNotifications
 346 |         description: Whether to send notifications about the creation of the new event
 347 |       - in: query
 348 |         name: supportsAttachments
 349 |         description: Whether API client performing operation supports event attachments
 350 |       responses:
 351 |         200:
 352 |           description: OK
 353 |       tags:
 354 |       - Event
 355 |   /calendars/{calendarId}/events/import:
 356 |     post:
 357 |       summary: Import Event
 358 |       description: Imports an event
 359 |       operationId: calendar.events.import
 360 |       parameters:
 361 |       - in: body
 362 |         name: body
 363 |         schema:
 364 |           $ref: '#/definitions/holder'
 365 |       - in: path
 366 |         name: calendarId
 367 |         description: Calendar identifier
 368 |       - in: query
 369 |         name: supportsAttachments
 370 |         description: Whether API client performing operation supports event attachments
 371 |       responses:
 372 |         200:
 373 |           description: OK
 374 |       tags:
 375 |       - Event
 376 |   /calendars/{calendarId}/events/quickAdd:
 377 |     post:
 378 |       summary: Create Event
 379 |       description: Creates an event based on a simple text string
 380 |       operationId: calendar.events.quickAdd
 381 |       parameters:
 382 |       - in: path
 383 |         name: calendarId
 384 |         description: Calendar identifier
 385 |       - in: query
 386 |         name: sendNotifications
 387 |         description: Whether to send notifications about the creation of the event
 388 |       - in: query
 389 |         name: text
 390 |         description: The text describing the event to be created
 391 |       responses:
 392 |         200:
 393 |           description: OK
 394 |       tags:
 395 |       - Event
 396 |   /calendars/{calendarId}/events/watch:
 397 |     post:
 398 |       summary: Watch Event
 399 |       description: Watch for changes to Events resources
 400 |       operationId: calendar.events.watch
 401 |       parameters:
 402 |       - in: query
 403 |         name: alwaysIncludeEmail
 404 |         description: Whether to always include a value in the email field for the
 405 |           organizer, creator and attendees, even if no real email is available (i
 406 |       - in: path
 407 |         name: calendarId
 408 |         description: Calendar identifier
 409 |       - in: query
 410 |         name: iCalUID
 411 |         description: Specifies event ID in the iCalendar format to be included in
 412 |           the response
 413 |       - in: query
 414 |         name: maxAttendees
 415 |         description: The maximum number of attendees to include in the response
 416 |       - in: query
 417 |         name: maxResults
 418 |         description: Maximum number of events returned on one result page
 419 |       - in: query
 420 |         name: orderBy
 421 |         description: The order of the events returned in the result
 422 |       - in: query
 423 |         name: pageToken
 424 |         description: Token specifying which result page to return
 425 |       - in: query
 426 |         name: privateExtendedProperty
 427 |         description: Extended properties constraint specified as propertyName=value
 428 |       - in: query
 429 |         name: q
 430 |         description: Free text search terms to find events that match these terms
 431 |           in any field, except for extended properties
 432 |       - in: body
 433 |         name: resource
 434 |         schema:
 435 |           $ref: '#/definitions/holder'
 436 |       - in: query
 437 |         name: sharedExtendedProperty
 438 |         description: Extended properties constraint specified as propertyName=value
 439 |       - in: query
 440 |         name: showDeleted
 441 |         description: Whether to include deleted events (with status equals "cancelled")
 442 |           in the result
 443 |       - in: query
 444 |         name: showHiddenInvitations
 445 |         description: Whether to include hidden invitations in the result
 446 |       - in: query
 447 |         name: singleEvents
 448 |         description: Whether to expand recurring events into instances and only return
 449 |           single one-off events and instances of recurring events, but not the underlying
 450 |           recurring events themselves
 451 |       - in: query
 452 |         name: syncToken
 453 |         description: Token obtained from the nextSyncToken field returned on the last
 454 |           page of results from the previous list request
 455 |       - in: query
 456 |         name: timeMax
 457 |         description: Upper bound (exclusive) for an event's start time to filter by
 458 |       - in: query
 459 |         name: timeMin
 460 |         description: Lower bound (inclusive) for an event's end time to filter by
 461 |       - in: query
 462 |         name: timeZone
 463 |         description: Time zone used in the response
 464 |       - in: query
 465 |         name: updatedMin
 466 |         description: Lower bound for an event's last modification time (as a RFC3339
 467 |           timestamp) to filter by
 468 |       responses:
 469 |         200:
 470 |           description: OK
 471 |       tags:
 472 |       - Event
 473 |   /calendars/{calendarId}/events/{eventId}:
 474 |     delete:
 475 |       summary: Delete Event
 476 |       description: Deletes an event
 477 |       operationId: calendar.events.delete
 478 |       parameters:
 479 |       - in: path
 480 |         name: calendarId
 481 |         description: Calendar identifier
 482 |       - in: path
 483 |         name: eventId
 484 |         description: Event identifier
 485 |       - in: query
 486 |         name: sendNotifications
 487 |         description: Whether to send notifications about the deletion of the event
 488 |       responses:
 489 |         200:
 490 |           description: OK
 491 |       tags:
 492 |       - Event
 493 |     get:
 494 |       summary: Get Event
 495 |       description: Returns an event
 496 |       operationId: calendar.events.get
 497 |       parameters:
 498 |       - in: query
 499 |         name: alwaysIncludeEmail
 500 |         description: Whether to always include a value in the email field for the
 501 |           organizer, creator and attendees, even if no real email is available (i
 502 |       - in: path
 503 |         name: calendarId
 504 |         description: Calendar identifier
 505 |       - in: path
 506 |         name: eventId
 507 |         description: Event identifier
 508 |       - in: query
 509 |         name: maxAttendees
 510 |         description: The maximum number of attendees to include in the response
 511 |       - in: query
 512 |         name: timeZone
 513 |         description: Time zone used in the response
 514 |       responses:
 515 |         200:
 516 |           description: OK
 517 |       tags:
 518 |       - Event
 519 |     patch:
 520 |       summary: Update Event
 521 |       description: Updates an event
 522 |       operationId: calendar.events.patch
 523 |       parameters:
 524 |       - in: query
 525 |         name: alwaysIncludeEmail
 526 |         description: Whether to always include a value in the email field for the
 527 |           organizer, creator and attendees, even if no real email is available (i
 528 |       - in: body
 529 |         name: body
 530 |         schema:
 531 |           $ref: '#/definitions/holder'
 532 |       - in: path
 533 |         name: calendarId
 534 |         description: Calendar identifier
 535 |       - in: path
 536 |         name: eventId
 537 |         description: Event identifier
 538 |       - in: query
 539 |         name: maxAttendees
 540 |         description: The maximum number of attendees to include in the response
 541 |       - in: query
 542 |         name: sendNotifications
 543 |         description: Whether to send notifications about the event update (e
 544 |       - in: query
 545 |         name: supportsAttachments
 546 |         description: Whether API client performing operation supports event attachments
 547 |       responses:
 548 |         200:
 549 |           description: OK
 550 |       tags:
 551 |       - Event
 552 |     put:
 553 |       summary: Update Event
 554 |       description: Updates an event
 555 |       operationId: calendar.events.update
 556 |       parameters:
 557 |       - in: query
 558 |         name: alwaysIncludeEmail
 559 |         description: Whether to always include a value in the email field for the
 560 |           organizer, creator and attendees, even if no real email is available (i
 561 |       - in: body
 562 |         name: body
 563 |         schema:
 564 |           $ref: '#/definitions/holder'
 565 |       - in: path
 566 |         name: calendarId
 567 |         description: Calendar identifier
 568 |       - in: path
 569 |         name: eventId
 570 |         description: Event identifier
 571 |       - in: query
 572 |         name: maxAttendees
 573 |         description: The maximum number of attendees to include in the response
 574 |       - in: query
 575 |         name: sendNotifications
 576 |         description: Whether to send notifications about the event update (e
 577 |       - in: query
 578 |         name: supportsAttachments
 579 |         description: Whether API client performing operation supports event attachments
 580 |       responses:
 581 |         200:
 582 |           description: OK
 583 |       tags:
 584 |       - Event
 585 |   /calendars/{calendarId}/events/{eventId}/instances:
 586 |     get:
 587 |       summary: Get Event Instance
 588 |       description: Returns instances of the specified recurring event
 589 |       operationId: calendar.events.instances
 590 |       parameters:
 591 |       - in: query
 592 |         name: alwaysIncludeEmail
 593 |         description: Whether to always include a value in the email field for the
 594 |           organizer, creator and attendees, even if no real email is available (i
 595 |       - in: path
 596 |         name: calendarId
 597 |         description: Calendar identifier
 598 |       - in: path
 599 |         name: eventId
 600 |         description: Recurring event identifier
 601 |       - in: query
 602 |         name: maxAttendees
 603 |         description: The maximum number of attendees to include in the response
 604 |       - in: query
 605 |         name: maxResults
 606 |         description: Maximum number of events returned on one result page
 607 |       - in: query
 608 |         name: originalStart
 609 |         description: The original start time of the instance in the result
 610 |       - in: query
 611 |         name: pageToken
 612 |         description: Token specifying which result page to return
 613 |       - in: query
 614 |         name: showDeleted
 615 |         description: Whether to include deleted events (with status equals "cancelled")
 616 |           in the result
 617 |       - in: query
 618 |         name: timeMax
 619 |         description: Upper bound (exclusive) for an event's start time to filter by
 620 |       - in: query
 621 |         name: timeMin
 622 |         description: Lower bound (inclusive) for an event's end time to filter by
 623 |       - in: query
 624 |         name: timeZone
 625 |         description: Time zone used in the response
 626 |       responses:
 627 |         200:
 628 |           description: OK
 629 |       tags:
 630 |       - Event
 631 |   /calendars/{calendarId}/events/{eventId}/move:
 632 |     post:
 633 |       summary: Move Event
 634 |       description: Moves an event to another calendar, i
 635 |       operationId: calendar.events.move
 636 |       parameters:
 637 |       - in: path
 638 |         name: calendarId
 639 |         description: Calendar identifier of the source calendar where the event currently
 640 |           is on
 641 |       - in: query
 642 |         name: destination
 643 |         description: Calendar identifier of the target calendar where the event is
 644 |           to be moved to
 645 |       - in: path
 646 |         name: eventId
 647 |         description: Event identifier
 648 |       - in: query
 649 |         name: sendNotifications
 650 |         description: Whether to send notifications about the change of the event's
 651 |           organizer
 652 |       responses:
 653 |         200:
 654 |           description: OK
 655 |       tags:
 656 |       - Event
 657 |   /channels/stop:
 658 |     post:
 659 |       summary: Stop Watching Resource
 660 |       description: Stop watching resources through this channel
 661 |       operationId: calendar.channels.stop
 662 |       parameters:
 663 |       - in: body
 664 |         name: resource
 665 |         schema:
 666 |           $ref: '#/definitions/holder'
 667 |       responses:
 668 |         200:
 669 |           description: OK
 670 |       tags:
 671 |       - Watch
 672 |   /colors:
 673 |     get:
 674 |       summary: Get Colors
 675 |       description: Returns the color definitions for calendars and events
 676 |       operationId: calendar.colors.get
 677 |       responses:
 678 |         200:
 679 |           description: OK
 680 |       tags:
 681 |       - Color
 682 |   /freeBusy:
 683 |     post:
 684 |       summary: Return Free/Busy Information
 685 |       description: Returns free/busy information for a set of calendars
 686 |       operationId: calendar.freebusy.query
 687 |       parameters:
 688 |       - in: body
 689 |         name: body
 690 |         schema:
 691 |           $ref: '#/definitions/holder'
 692 |       responses:
 693 |         200:
 694 |           description: OK
 695 |       tags:
 696 |       - Free/Busy
 697 |   /users/me/calendarList:
 698 |     get:
 699 |       summary: Return Entries
 700 |       description: Returns entries on the user's calendar list
 701 |       operationId: calendar.calendarList.list
 702 |       parameters:
 703 |       - in: query
 704 |         name: maxResults
 705 |         description: Maximum number of entries returned on one result page
 706 |       - in: query
 707 |         name: minAccessRole
 708 |         description: The minimum access role for the user in the returned entries
 709 |       - in: query
 710 |         name: pageToken
 711 |         description: Token specifying which result page to return
 712 |       - in: query
 713 |         name: showDeleted
 714 |         description: Whether to include deleted calendar list entries in the result
 715 |       - in: query
 716 |         name: showHidden
 717 |         description: Whether to show hidden entries
 718 |       - in: query
 719 |         name: syncToken
 720 |         description: Token obtained from the nextSyncToken field returned on the last
 721 |           page of results from the previous list request
 722 |       responses:
 723 |         200:
 724 |           description: OK
 725 |       tags:
 726 |       - Event
 727 |     post:
 728 |       summary: Add Entry
 729 |       description: Adds an entry to the user's calendar list
 730 |       operationId: calendar.calendarList.insert
 731 |       parameters:
 732 |       - in: body
 733 |         name: body
 734 |         schema:
 735 |           $ref: '#/definitions/holder'
 736 |       - in: query
 737 |         name: colorRgbFormat
 738 |         description: Whether to use the foregroundColor and backgroundColor fields
 739 |           to write the calendar colors (RGB)
 740 |       responses:
 741 |         200:
 742 |           description: OK
 743 |       tags:
 744 |       - Event
 745 |   /users/me/calendarList/watch:
 746 |     post:
 747 |       summary: Watch Entry
 748 |       description: Watch for changes to CalendarList resources
 749 |       operationId: calendar.calendarList.watch
 750 |       parameters:
 751 |       - in: query
 752 |         name: maxResults
 753 |         description: Maximum number of entries returned on one result page
 754 |       - in: query
 755 |         name: minAccessRole
 756 |         description: The minimum access role for the user in the returned entries
 757 |       - in: query
 758 |         name: pageToken
 759 |         description: Token specifying which result page to return
 760 |       - in: body
 761 |         name: resource
 762 |         schema:
 763 |           $ref: '#/definitions/holder'
 764 |       - in: query
 765 |         name: showDeleted
 766 |         description: Whether to include deleted calendar list entries in the result
 767 |       - in: query
 768 |         name: showHidden
 769 |         description: Whether to show hidden entries
 770 |       - in: query
 771 |         name: syncToken
 772 |         description: Token obtained from the nextSyncToken field returned on the last
 773 |           page of results from the previous list request
 774 |       responses:
 775 |         200:
 776 |           description: OK
 777 |       tags:
 778 |       - Event
 779 |   /users/me/calendarList/{calendarId}:
 780 |     delete:
 781 |       summary: Delete Entry
 782 |       description: Deletes an entry on the user's calendar list
 783 |       operationId: calendar.calendarList.delete
 784 |       parameters:
 785 |       - in: path
 786 |         name: calendarId
 787 |         description: Calendar identifier
 788 |       responses:
 789 |         200:
 790 |           description: OK
 791 |       tags:
 792 |       - Event
 793 |     get:
 794 |       summary: Get Entry
 795 |       description: Returns an entry on the user's calendar list
 796 |       operationId: calendar.calendarList.get
 797 |       parameters:
 798 |       - in: path
 799 |         name: calendarId
 800 |         description: Calendar identifier
 801 |       responses:
 802 |         200:
 803 |           description: OK
 804 |       tags:
 805 |       - Event
 806 |     patch:
 807 |       summary: Update Entry
 808 |       description: Updates an entry on the user's calendar list
 809 |       operationId: calendar.calendarList.patch
 810 |       parameters:
 811 |       - in: body
 812 |         name: body
 813 |         schema:
 814 |           $ref: '#/definitions/holder'
 815 |       - in: path
 816 |         name: calendarId
 817 |         description: Calendar identifier
 818 |       - in: query
 819 |         name: colorRgbFormat
 820 |         description: Whether to use the foregroundColor and backgroundColor fields
 821 |           to write the calendar colors (RGB)
 822 |       responses:
 823 |         200:
 824 |           description: OK
 825 |       tags:
 826 |       - Event
 827 |     put:
 828 |       summary: Update Entry
 829 |       description: Updates an entry on the user's calendar list
 830 |       operationId: calendar.calendarList.update
 831 |       parameters:
 832 |       - in: body
 833 |         name: body
 834 |         schema:
 835 |           $ref: '#/definitions/holder'
 836 |       - in: path
 837 |         name: calendarId
 838 |         description: Calendar identifier
 839 |       - in: query
 840 |         name: colorRgbFormat
 841 |         description: Whether to use the foregroundColor and backgroundColor fields
 842 |           to write the calendar colors (RGB)
 843 |       responses:
 844 |         200:
 845 |           description: OK
 846 |       tags:
 847 |       - Event
 848 |   /users/me/settings:
 849 |     get:
 850 |       summary: Get Settings
 851 |       description: Returns all user settings for the authenticated user
 852 |       operationId: calendar.settings.list
 853 |       parameters:
 854 |       - in: query
 855 |         name: maxResults
 856 |         description: Maximum number of entries returned on one result page
 857 |       - in: query
 858 |         name: pageToken
 859 |         description: Token specifying which result page to return
 860 |       - in: query
 861 |         name: syncToken
 862 |         description: Token obtained from the nextSyncToken field returned on the last
 863 |           page of results from the previous list request
 864 |       responses:
 865 |         200:
 866 |           description: OK
 867 |       tags:
 868 |       - Setting
 869 |   /users/me/settings/watch:
 870 |     post:
 871 |       summary: Watch Settings
 872 |       description: Watch for changes to Settings resources
 873 |       operationId: calendar.settings.watch
 874 |       parameters:
 875 |       - in: query
 876 |         name: maxResults
 877 |         description: Maximum number of entries returned on one result page
 878 |       - in: query
 879 |         name: pageToken
 880 |         description: Token specifying which result page to return
 881 |       - in: body
 882 |         name: resource
 883 |         schema:
 884 |           $ref: '#/definitions/holder'
 885 |       - in: query
 886 |         name: syncToken
 887 |         description: Token obtained from the nextSyncToken field returned on the last
 888 |           page of results from the previous list request
 889 |       responses:
 890 |         200:
 891 |           description: OK
 892 |       tags:
 893 |       - Setting
 894 |   /users/me/settings/{setting}:
 895 |     get:
 896 |       summary: Get Setting
 897 |       description: Returns a single user setting
 898 |       operationId: calendar.settings.get
 899 |       parameters:
 900 |       - in: path
 901 |         name: setting
 902 |         description: The id of the user setting
 903 |       responses:
 904 |         200:
 905 |           description: OK
 906 |       tags:
 907 |       - Setting
 908 | definitions:
 909 |   Acl:
 910 |     properties:
 911 |       etag:
 912 |         description: This is a default description.
 913 |         type: parameters
 914 |       items:
 915 |         description: This is a default description.
 916 |         type: parameters
 917 |       kind:
 918 |         description: This is a default description.
 919 |         type: parameters
 920 |       nextPageToken:
 921 |         description: This is a default description.
 922 |         type: parameters
 923 |       nextSyncToken:
 924 |         description: This is a default description.
 925 |         type: parameters
 926 |   AclRule:
 927 |     properties:
 928 |       etag:
 929 |         description: This is a default description.
 930 |         type: parameters
 931 |       id:
 932 |         description: This is a default description.
 933 |         type: parameters
 934 |       kind:
 935 |         description: This is a default description.
 936 |         type: parameters
 937 |       role:
 938 |         description: This is a default description.
 939 |         type: parameters
 940 |       scope:
 941 |         description: This is a default description.
 942 |         type: parameters
 943 |   Calendar:
 944 |     properties:
 945 |       description:
 946 |         description: This is a default description.
 947 |         type: parameters
 948 |       etag:
 949 |         description: This is a default description.
 950 |         type: parameters
 951 |       id:
 952 |         description: This is a default description.
 953 |         type: parameters
 954 |       kind:
 955 |         description: This is a default description.
 956 |         type: parameters
 957 |       location:
 958 |         description: This is a default description.
 959 |         type: parameters
 960 |       summary:
 961 |         description: This is a default description.
 962 |         type: parameters
 963 |       timeZone:
 964 |         description: This is a default description.
 965 |         type: parameters
 966 |   CalendarList:
 967 |     properties:
 968 |       etag:
 969 |         description: This is a default description.
 970 |         type: parameters
 971 |       items:
 972 |         description: This is a default description.
 973 |         type: parameters
 974 |       kind:
 975 |         description: This is a default description.
 976 |         type: parameters
 977 |       nextPageToken:
 978 |         description: This is a default description.
 979 |         type: parameters
 980 |       nextSyncToken:
 981 |         description: This is a default description.
 982 |         type: parameters
 983 |   CalendarListEntry:
 984 |     properties:
 985 |       accessRole:
 986 |         description: This is a default description.
 987 |         type: parameters
 988 |       backgroundColor:
 989 |         description: This is a default description.
 990 |         type: parameters
 991 |       colorId:
 992 |         description: This is a default description.
 993 |         type: parameters
 994 |       defaultReminders:
 995 |         description: This is a default description.
 996 |         type: parameters
 997 |       deleted:
 998 |         description: This is a default description.
 999 |         type: parameters
1000 |       description:
1001 |         description: This is a default description.
1002 |         type: parameters
1003 |       etag:
1004 |         description: This is a default description.
1005 |         type: parameters
1006 |       foregroundColor:
1007 |         description: This is a default description.
1008 |         type: parameters
1009 |       hidden:
1010 |         description: This is a default description.
1011 |         type: parameters
1012 |       id:
1013 |         description: This is a default description.
1014 |         type: parameters
1015 |       kind:
1016 |         description: This is a default description.
1017 |         type: parameters
1018 |       location:
1019 |         description: This is a default description.
1020 |         type: parameters
1021 |       notificationSettings:
1022 |         description: This is a default description.
1023 |         type: parameters
1024 |       primary:
1025 |         description: This is a default description.
1026 |         type: parameters
1027 |       selected:
1028 |         description: This is a default description.
1029 |         type: parameters
1030 |       summary:
1031 |         description: This is a default description.
1032 |         type: parameters
1033 |       summaryOverride:
1034 |         description: This is a default description.
1035 |         type: parameters
1036 |       timeZone:
1037 |         description: This is a default description.
1038 |         type: parameters
1039 |   CalendarNotification:
1040 |     properties:
1041 |       method:
1042 |         description: This is a default description.
1043 |         type: parameters
1044 |       type:
1045 |         description: This is a default description.
1046 |         type: parameters
1047 |   Channel:
1048 |     properties:
1049 |       address:
1050 |         description: This is a default description.
1051 |         type: parameters
1052 |       expiration:
1053 |         description: This is a default description.
1054 |         type: parameters
1055 |       id:
1056 |         description: This is a default description.
1057 |         type: parameters
1058 |       kind:
1059 |         description: This is a default description.
1060 |         type: parameters
1061 |       params:
1062 |         description: This is a default description.
1063 |         type: parameters
1064 |       payload:
1065 |         description: This is a default description.
1066 |         type: parameters
1067 |       resourceId:
1068 |         description: This is a default description.
1069 |         type: parameters
1070 |       resourceUri:
1071 |         description: This is a default description.
1072 |         type: parameters
1073 |       token:
1074 |         description: This is a default description.
1075 |         type: parameters
1076 |       type:
1077 |         description: This is a default description.
1078 |         type: parameters
1079 |   ColorDefinition:
1080 |     properties:
1081 |       background:
1082 |         description: This is a default description.
1083 |         type: parameters
1084 |       foreground:
1085 |         description: This is a default description.
1086 |         type: parameters
1087 |   Colors:
1088 |     properties:
1089 |       calendar:
1090 |         description: This is a default description.
1091 |         type: parameters
1092 |       event:
1093 |         description: This is a default description.
1094 |         type: parameters
1095 |       kind:
1096 |         description: This is a default description.
1097 |         type: parameters
1098 |       updated:
1099 |         description: This is a default description.
1100 |         type: parameters
1101 |   Error:
1102 |     properties:
1103 |       domain:
1104 |         description: This is a default description.
1105 |         type: parameters
1106 |       reason:
1107 |         description: This is a default description.
1108 |         type: parameters
1109 |   Event:
1110 |     properties:
1111 |       anyoneCanAddSelf:
1112 |         description: This is a default description.
1113 |         type: parameters
1114 |       attachments:
1115 |         description: This is a default description.
1116 |         type: parameters
1117 |       attendees:
1118 |         description: This is a default description.
1119 |         type: parameters
1120 |       attendeesOmitted:
1121 |         description: This is a default description.
1122 |         type: parameters
1123 |       colorId:
1124 |         description: This is a default description.
1125 |         type: parameters
1126 |       created:
1127 |         description: This is a default description.
1128 |         type: parameters
1129 |       creator:
1130 |         description: This is a default description.
1131 |         type: parameters
1132 |       description:
1133 |         description: This is a default description.
1134 |         type: parameters
1135 |       endTimeUnspecified:
1136 |         description: This is a default description.
1137 |         type: parameters
1138 |       etag:
1139 |         description: This is a default description.
1140 |         type: parameters
1141 |       extendedProperties:
1142 |         description: This is a default description.
1143 |         type: parameters
1144 |       gadget:
1145 |         description: This is a default description.
1146 |         type: parameters
1147 |       guestsCanInviteOthers:
1148 |         description: This is a default description.
1149 |         type: parameters
1150 |       guestsCanModify:
1151 |         description: This is a default description.
1152 |         type: parameters
1153 |       guestsCanSeeOtherGuests:
1154 |         description: This is a default description.
1155 |         type: parameters
1156 |       hangoutLink:
1157 |         description: This is a default description.
1158 |         type: parameters
1159 |       htmlLink:
1160 |         description: This is a default description.
1161 |         type: parameters
1162 |       iCalUID:
1163 |         description: This is a default description.
1164 |         type: parameters
1165 |       id:
1166 |         description: This is a default description.
1167 |         type: parameters
1168 |       kind:
1169 |         description: This is a default description.
1170 |         type: parameters
1171 |       location:
1172 |         description: This is a default description.
1173 |         type: parameters
1174 |       locked:
1175 |         description: This is a default description.
1176 |         type: parameters
1177 |       organizer:
1178 |         description: This is a default description.
1179 |         type: parameters
1180 |       privateCopy:
1181 |         description: This is a default description.
1182 |         type: parameters
1183 |       recurrence:
1184 |         description: This is a default description.
1185 |         type: parameters
1186 |       recurringEventId:
1187 |         description: This is a default description.
1188 |         type: parameters
1189 |       reminders:
1190 |         description: This is a default description.
1191 |         type: parameters
1192 |       sequence:
1193 |         description: This is a default description.
1194 |         type: parameters
1195 |       source:
1196 |         description: This is a default description.
1197 |         type: parameters
1198 |       status:
1199 |         description: This is a default description.
1200 |         type: parameters
1201 |       summary:
1202 |         description: This is a default description.
1203 |         type: parameters
1204 |       transparency:
1205 |         description: This is a default description.
1206 |         type: parameters
1207 |       updated:
1208 |         description: This is a default description.
1209 |         type: parameters
1210 |       visibility:
1211 |         description: This is a default description.
1212 |         type: parameters
1213 |   EventAttachment:
1214 |     properties:
1215 |       fileId:
1216 |         description: This is a default description.
1217 |         type: parameters
1218 |       fileUrl:
1219 |         description: This is a default description.
1220 |         type: parameters
1221 |       iconLink:
1222 |         description: This is a default description.
1223 |         type: parameters
1224 |       mimeType:
1225 |         description: This is a default description.
1226 |         type: parameters
1227 |       title:
1228 |         description: This is a default description.
1229 |         type: parameters
1230 |   EventAttendee:
1231 |     properties:
1232 |       additionalGuests:
1233 |         description: This is a default description.
1234 |         type: parameters
1235 |       comment:
1236 |         description: This is a default description.
1237 |         type: parameters
1238 |       displayName:
1239 |         description: This is a default description.
1240 |         type: parameters
1241 |       email:
1242 |         description: This is a default description.
1243 |         type: parameters
1244 |       id:
1245 |         description: This is a default description.
1246 |         type: parameters
1247 |       optional:
1248 |         description: This is a default description.
1249 |         type: parameters
1250 |       organizer:
1251 |         description: This is a default description.
1252 |         type: parameters
1253 |       resource:
1254 |         description: This is a default description.
1255 |         type: parameters
1256 |       responseStatus:
1257 |         description: This is a default description.
1258 |         type: parameters
1259 |       self:
1260 |         description: This is a default description.
1261 |         type: parameters
1262 |   EventDateTime:
1263 |     properties:
1264 |       date:
1265 |         description: This is a default description.
1266 |         type: parameters
1267 |       dateTime:
1268 |         description: This is a default description.
1269 |         type: parameters
1270 |       timeZone:
1271 |         description: This is a default description.
1272 |         type: parameters
1273 |   EventReminder:
1274 |     properties:
1275 |       method:
1276 |         description: This is a default description.
1277 |         type: parameters
1278 |       minutes:
1279 |         description: This is a default description.
1280 |         type: parameters
1281 |   Events:
1282 |     properties:
1283 |       accessRole:
1284 |         description: This is a default description.
1285 |         type: parameters
1286 |       defaultReminders:
1287 |         description: This is a default description.
1288 |         type: parameters
1289 |       description:
1290 |         description: This is a default description.
1291 |         type: parameters
1292 |       etag:
1293 |         description: This is a default description.
1294 |         type: parameters
1295 |       items:
1296 |         description: This is a default description.
1297 |         type: parameters
1298 |       kind:
1299 |         description: This is a default description.
1300 |         type: parameters
1301 |       nextPageToken:
1302 |         description: This is a default description.
1303 |         type: parameters
1304 |       nextSyncToken:
1305 |         description: This is a default description.
1306 |         type: parameters
1307 |       summary:
1308 |         description: This is a default description.
1309 |         type: parameters
1310 |       timeZone:
1311 |         description: This is a default description.
1312 |         type: parameters
1313 |       updated:
1314 |         description: This is a default description.
1315 |         type: parameters
1316 |   FreeBusyCalendar:
1317 |     properties:
1318 |       busy:
1319 |         description: This is a default description.
1320 |         type: parameters
1321 |       errors:
1322 |         description: This is a default description.
1323 |         type: parameters
1324 |   FreeBusyGroup:
1325 |     properties:
1326 |       calendars:
1327 |         description: This is a default description.
1328 |         type: parameters
1329 |       errors:
1330 |         description: This is a default description.
1331 |         type: parameters
1332 |   FreeBusyRequest:
1333 |     properties:
1334 |       calendarExpansionMax:
1335 |         description: This is a default description.
1336 |         type: parameters
1337 |       groupExpansionMax:
1338 |         description: This is a default description.
1339 |         type: parameters
1340 |       items:
1341 |         description: This is a default description.
1342 |         type: parameters
1343 |       timeMax:
1344 |         description: This is a default description.
1345 |         type: parameters
1346 |       timeMin:
1347 |         description: This is a default description.
1348 |         type: parameters
1349 |       timeZone:
1350 |         description: This is a default description.
1351 |         type: parameters
1352 |   FreeBusyRequestItem:
1353 |     properties:
1354 |       id:
1355 |         description: This is a default description.
1356 |         type: parameters
1357 |   FreeBusyResponse:
1358 |     properties:
1359 |       calendars:
1360 |         description: This is a default description.
1361 |         type: parameters
1362 |       groups:
1363 |         description: This is a default description.
1364 |         type: parameters
1365 |       kind:
1366 |         description: This is a default description.
1367 |         type: parameters
1368 |       timeMax:
1369 |         description: This is a default description.
1370 |         type: parameters
1371 |       timeMin:
1372 |         description: This is a default description.
1373 |         type: parameters
1374 |   Setting:
1375 |     properties:
1376 |       etag:
1377 |         description: This is a default description.
1378 |         type: parameters
1379 |       id:
1380 |         description: This is a default description.
1381 |         type: parameters
1382 |       kind:
1383 |         description: This is a default description.
1384 |         type: parameters
1385 |       value:
1386 |         description: This is a default description.
1387 |         type: parameters
1388 |   Settings:
1389 |     properties:
1390 |       etag:
1391 |         description: This is a default description.
1392 |         type: parameters
1393 |       items:
1394 |         description: This is a default description.
1395 |         type: parameters
1396 |       kind:
1397 |         description: This is a default description.
1398 |         type: parameters
1399 |       nextPageToken:
1400 |         description: This is a default description.
1401 |         type: parameters
1402 |       nextSyncToken:
1403 |         description: This is a default description.
1404 |         type: parameters
1405 |   TimePeriod:
1406 |     properties:
1407 |       end:
1408 |         description: This is a default description.
1409 |         type: parameters
1410 |       start:
1411 |         description: This is a default description.
1412 |         type: parameters
```
Page 1/3FirstPrevNextLast