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

```
├── GCalendar
│   ├── README.md
│   ├── requirements.txt
│   └── src
│       ├── calendar_service.py
│       ├── create_token.py
│       ├── list_past_events.py
│       ├── mcp_client.py
│       ├── mcp_server.py
│       └── renew_token.py
├── README.md
├── requirements.txt
└── src
    ├── calendar_service.py
    ├── create_token.py
    ├── list_past_events.py
    ├── mcp_client.py
    ├── mcp_server.py
    └── renew_token.py
```

# Files

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

```markdown
  1 | # Google Calendar MCP Server - Python Installation Guide
  2 | 
  3 | 📘 **Overview**
  4 | A Model Context Protocol server that provides access to Google Calendar API with support for asynchronous operations. This implementation enables efficient calendar management through a standardized interface.
  5 | 
  6 | 🔑 **Key Components**
  7 | 
  8 | ### API Tools
  9 | - list: Query calendar events (past 2 years to 1 year future)
 10 | - create-event: Create new calendar entries
 11 | - delete-duplicates: Remove duplicate events
 12 | - delete-event: Delete specific calendar events
 13 | 
 14 | ### Core Resources
 15 | - Token Management System (credentials/*.json)
 16 |   - OAuth 2.0 authentication flow
 17 |   - Automatic token refresh handling
 18 |   - Runtime token discovery
 19 | - Logging System (logs/*.log)
 20 |   - Operation logging with timestamps
 21 |   - Error tracking and debugging
 22 |   - Performance metrics collection
 23 | 
 24 | 💡 **Implementation Steps**
 25 | 
 26 | ### Prerequisites
 27 | ```bash
 28 | # Install required Python packages
 29 | pip install -r requirements.txt
 30 | ```
 31 | 
 32 | Dependencies:
 33 | ```python
 34 | google-auth-oauthlib==1.0.0
 35 | google-auth-httplib2==0.1.0
 36 | google-api-python-client==2.108.0
 37 | aiohttp==3.8.5
 38 | asyncio==3.4.3
 39 | ```
 40 | 
 41 | ### Initial Setup
 42 | ```bash
 43 | # Clone repository
 44 | git clone https://github.com/yourusername/GCalendar.git
 45 | cd GCalendar
 46 | 
 47 | # Create required directories
 48 | mkdir -p credentials logs
 49 | ```
 50 | 
 51 | ### Authentication Setup
 52 | 1. Create Google Cloud Console project
 53 | 2. Enable Google Calendar API
 54 | 3. Download credentials.json to credentials/
 55 | 4. Run create_token.py:
 56 | ```bash
 57 | python src/create_token.py
 58 | ```
 59 | 
 60 | ⚙️ **Technical Configuration**
 61 | 
 62 | ### Server Configuration
 63 | Add to `claude_desktop_config.json`:
 64 | ```json
 65 | {
 66 |   "mcpServers": {
 67 |     "gcalendar": {
 68 |       "command": "YOUR_CONDA_PATH/envs/mcp-gcalendar/bin/python",
 69 |       "args": [
 70 |         "YOUR_PATH/GCalendar/src/mcp_server.py"
 71 |       ]
 72 |     }
 73 |   }
 74 | }
 75 | ```
 76 | 
 77 | ### Path Configuration
 78 | Replace placeholders:
 79 | • YOUR_CONDA_PATH options:
 80 |   - M1/M2 Mac: `/opt/homebrew/Caskroom/miniforge/base`
 81 |   - Miniconda: `~/miniconda3` or `/opt/miniconda3`
 82 | • YOUR_PATH: Full repository clone path
 83 | 
 84 | ### Error Handling
 85 | - Automatic token refresh on expiry
 86 | - 60-second operation timeout
 87 | - Comprehensive error logging
 88 | - Auto-retry on transient errors
 89 | 
 90 | ### Project Structure
 91 | ```
 92 | GCalendar/
 93 | ├── credentials/
 94 | │   ├── credentials.json   # From Google Cloud Console
 95 | │   └── token.json        # Generated by create_token.py
 96 | ├── logs/
 97 | │   └── calendar_service.log
 98 | ├── src/
 99 | │   ├── calendar_service.py   # Core calendar operations
100 | │   ├── create_token.py      # Token generation
101 | │   ├── list_past_events.py  # Event listing utility
102 | │   ├── mcp_client.py        # MCP client implementation
103 | │   ├── mcp_server.py        # Main server implementation
104 | │   └── renew_token.py       # Token renewal utility
105 | ├── requirements.txt
106 | └── README.md
107 | ```
108 | 
109 | ### System Requirements
110 | - Python 3.9 or higher
111 | - Google Calendar API enabled
112 | - Valid OAuth 2.0 credentials
113 | - Internet connectivity
114 | - Asia/Bangkok timezone
115 | 
116 | ## License
117 | This project is licensed under the MIT License. See LICENSE file for details.
118 | 
```

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

```
1 | google-auth-oauthlib==1.0.0
2 | google-auth-httplib2==0.1.0
3 | google-api-python-client==2.108.0
4 | aiohttp==3.8.5
5 | asyncio==3.4.3
6 | mcp
```

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

```
1 | google-auth-oauthlib==1.0.0
2 | google-auth-httplib2==0.1.0
3 | google-api-python-client==2.108.0
4 | aiohttp==3.8.5
5 | asyncio==3.4.3
6 | tzdata==2023.3  # For timezone database
```

--------------------------------------------------------------------------------
/GCalendar/src/mcp_client.py:
--------------------------------------------------------------------------------

```python
 1 | from mcp import ClientSession, StdioServerParameters
 2 | from mcp.client.stdio import stdio_client
 3 | import asyncio
 4 | import logging
 5 | import os
 6 | 
 7 | # Set up logging
 8 | logging.basicConfig(
 9 |     level=logging.INFO,
10 |     format='[%(asctime)s] %(levelname)s: %(message)s',
11 |     datefmt='%Y-%m-%d %H:%M:%S'
12 | )
13 | logger = logging.getLogger(__name__)
14 | 
15 | async def run():
16 |     # Create server parameters
17 |     server_params = StdioServerParameters(
18 |         command="python",
19 |         args=[os.path.join(os.path.dirname(__file__), "mcp_server.py")]
20 |     )
21 |     
22 |     try:
23 |         async with stdio_client(server_params) as (read, write):
24 |             async with ClientSession(read, write) as session:
25 |                 # Initialize connection
26 |                 await session.initialize()
27 |                 logger.info("Initialization successful")
28 |                 
29 |                 # List calendar events using tool
30 |                 result = await session.call_tool("list", {})
31 |                 if result and hasattr(result, "content"):
32 |                     for content_item in result.content:
33 |                         if hasattr(content_item, "text"):
34 |                             print("\nCalendar Events:\n")
35 |                             print(content_item.text)
36 |                 else:
37 |                     logger.warning("No events data received")
38 |                 
39 |     except Exception as e:
40 |         logger.error(f"Error: {e}", exc_info=True)
41 | 
42 | if __name__ == "__main__":
43 |     try:
44 |         asyncio.run(run())
45 |     except KeyboardInterrupt:
46 |         logger.info("Client shutdown requested")
```

--------------------------------------------------------------------------------
/src/mcp_client.py:
--------------------------------------------------------------------------------

```python
 1 | from mcp import ClientSession, StdioServerParameters
 2 | from mcp.client.stdio import stdio_client
 3 | import asyncio
 4 | import logging
 5 | import os
 6 | 
 7 | # Set up logging
 8 | logging.basicConfig(
 9 |     level=logging.INFO,
10 |     format='[%(asctime)s] %(levelname)s: %(message)s',
11 |     datefmt='%Y-%m-%d %H:%M:%S'
12 | )
13 | logger = logging.getLogger(__name__)
14 | 
15 | async def run():
16 |     # Create server parameters
17 |     server_params = StdioServerParameters(
18 |         command="python",
19 |         args=[os.path.join(os.path.dirname(__file__), "mcp_server.py")]
20 |     )
21 |     
22 |     try:
23 |         async with stdio_client(server_params) as (read, write):
24 |             async with ClientSession(read, write) as session:
25 |                 # Initialize connection
26 |                 await session.initialize()
27 |                 logger.info("Initialization successful")
28 |                 
29 |                 # List calendar events using tool
30 |                 result = await session.call_tool("list", {})
31 |                 if result and hasattr(result, "content"):
32 |                     for content_item in result.content:
33 |                         if hasattr(content_item, "text"):
34 |                             print("\nCalendar Events:\n")
35 |                             print(content_item.text)
36 |                 else:
37 |                     logger.warning("No events data received")
38 |                 
39 |     except Exception as e:
40 |         logger.error(f"Error: {e}", exc_info=True)
41 | 
42 | if __name__ == "__main__":
43 |     try:
44 |         asyncio.run(run())
45 |     except KeyboardInterrupt:
46 |         logger.info("Client shutdown requested")
```

--------------------------------------------------------------------------------
/GCalendar/src/mcp_server.py:
--------------------------------------------------------------------------------

```python
  1 | from mcp.server import Server, NotificationOptions
  2 | from mcp.server.models import InitializationOptions
  3 | import mcp.server.stdio
  4 | import mcp.types as types
  5 | import logging
  6 | import asyncio
  7 | from calendar_service import CalendarService
  8 | import os
  9 | import signal
 10 | from datetime import datetime
 11 | 
 12 | # Set up logging
 13 | logging.basicConfig(
 14 |     level=logging.INFO,
 15 |     format='[%(asctime)s] %(levelname)s: %(message)s',
 16 |     datefmt='%Y-%m-%d %H:%M:%S'
 17 | )
 18 | logger = logging.getLogger(__name__)
 19 | 
 20 | # Create a server instance
 21 | server = Server("gcalendar-server")
 22 | calendar_service = None
 23 | 
 24 | # Signal handler for graceful shutdown
 25 | def signal_handler(signum, frame):
 26 |     logger.info(f"Received signal {signum}")
 27 |     if calendar_service:
 28 |         calendar_service.close()
 29 |     raise KeyboardInterrupt
 30 | 
 31 | signal.signal(signal.SIGINT, signal_handler)
 32 | signal.signal(signal.SIGTERM, signal_handler)
 33 | 
 34 | @server.list_tools()
 35 | async def handle_list_tools() -> list[types.Tool]:
 36 |     return [
 37 |         types.Tool(
 38 |             name="list",
 39 |             description="List calendar events",
 40 |             arguments={},
 41 |             inputSchema={
 42 |                 "type": "object",
 43 |                 "properties": {}
 44 |             }
 45 |         ),
 46 |         types.Tool(
 47 |             name="create-event",
 48 |             description="Create a new calendar event",
 49 |             arguments={},
 50 |             inputSchema={
 51 |                 "type": "object",
 52 |                 "properties": {
 53 |                     "summary": {
 54 |                         "type": "string",
 55 |                         "description": "Event title"
 56 |                     },
 57 |                     "start_time": {
 58 |                         "type": "string",
 59 |                         "description": "Start time in YYYY-MM-DDTHH:MM:SS format or date in YYYY-MM-DD format"
 60 |                     },
 61 |                     "end_time": {
 62 |                         "type": "string",
 63 |                         "description": "End time (optional). If not provided, event will be 1 hour long"
 64 |                     },
 65 |                     "description": {
 66 |                         "type": "string",
 67 |                         "description": "Event description (optional)"
 68 |                     }
 69 |                 },
 70 |                 "required": ["summary", "start_time"]
 71 |             }
 72 |         ),
 73 |         types.Tool(
 74 |             name="delete-duplicates",
 75 |             description="Delete duplicate events on a specific date",
 76 |             arguments={},
 77 |             inputSchema={
 78 |                 "type": "object",
 79 |                 "properties": {
 80 |                     "target_date": {
 81 |                         "type": "string",
 82 |                         "description": "Target date in YYYY-MM-DD format"
 83 |                     },
 84 |                     "event_summary": {
 85 |                         "type": "string",
 86 |                         "description": "Event title to match"
 87 |                     }
 88 |                 },
 89 |                 "required": ["target_date", "event_summary"]
 90 |             }
 91 |         ),
 92 |         types.Tool(
 93 |             name="delete-event",
 94 |             description="Delete a single calendar event",
 95 |             arguments={},
 96 |             inputSchema={
 97 |                 "type": "object",
 98 |                 "properties": {
 99 |                     "event_time": {
100 |                         "type": "string",
101 |                         "description": "Event time from list output"
102 |                     },
103 |                     "event_summary": {
104 |                         "type": "string",
105 |                         "description": "Event title to match"
106 |                     }
107 |                 },
108 |                 "required": ["event_time", "event_summary"]
109 |             }
110 |         )
111 |     ]
112 | 
113 | @server.call_tool()
114 | async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent]:
115 |     try:
116 |         if name == "list":
117 |             formatted_text = await asyncio.wait_for(
118 |                 calendar_service.list_events(),
119 |                 timeout=60
120 |             )
121 |             return [types.TextContent(
122 |                 type="text",
123 |                 text=formatted_text
124 |             )]
125 |             
126 |         elif name == "create-event":
127 |             if not arguments.get("summary"):
128 |                 return [types.TextContent(
129 |                     type="text",
130 |                     text="Error: Event title (summary) is required"
131 |                 )]
132 |             
133 |             if not arguments.get("start_time"):
134 |                 return [types.TextContent(
135 |                     type="text",
136 |                     text="Error: Start time is required"
137 |                 )]
138 |             
139 |             result = await asyncio.wait_for(
140 |                 calendar_service.create_event(
141 |                     summary=arguments["summary"],
142 |                     start_time=arguments["start_time"],
143 |                     end_time=arguments.get("end_time"),
144 |                     description=arguments.get("description")
145 |                 ),
146 |                 timeout=60
147 |             )
148 |             
149 |             return [types.TextContent(
150 |                 type="text",
151 |                 text=result
152 |             )]
153 |             
154 |         elif name == "delete-duplicates":
155 |             if not arguments.get("target_date"):
156 |                 return [types.TextContent(
157 |                     type="text",
158 |                     text="Error: Target date is required"
159 |                 )]
160 |             
161 |             if not arguments.get("event_summary"):
162 |                 return [types.TextContent(
163 |                     type="text",
164 |                     text="Error: Event title is required"
165 |                 )]
166 |             
167 |             result = await asyncio.wait_for(
168 |                 calendar_service.delete_duplicate_events(
169 |                     target_date=arguments["target_date"],
170 |                     event_summary=arguments["event_summary"]
171 |                 ),
172 |                 timeout=60
173 |             )
174 |             
175 |             return [types.TextContent(
176 |                 type="text",
177 |                 text=result
178 |             )]
179 | 
180 |         elif name == "delete-event":
181 |             if not arguments.get("event_time"):
182 |                 return [types.TextContent(
183 |                     type="text",
184 |                     text="Error: Event time is required"
185 |                 )]
186 |             
187 |             if not arguments.get("event_summary"):
188 |                 return [types.TextContent(
189 |                     type="text",
190 |                     text="Error: Event title is required"
191 |                 )]
192 |             
193 |             result = await asyncio.wait_for(
194 |                 calendar_service.delete_single_event(
195 |                     event_time=arguments["event_time"],
196 |                     event_summary=arguments["event_summary"]
197 |                 ),
198 |                 timeout=60
199 |             )
200 |             
201 |             return [types.TextContent(
202 |                 type="text",
203 |                 text=result
204 |             )]
205 |         
206 |         else:
207 |             return [types.TextContent(
208 |                 type="text",
209 |                 text=f"Unknown tool: {name}"
210 |             )]
211 |             
212 |     except asyncio.TimeoutError:
213 |         error_msg = f"Operation timed out while executing {name}"
214 |         logger.error(error_msg)
215 |         return [types.TextContent(
216 |             type="text",
217 |             text=error_msg
218 |         )]
219 |     except Exception as e:
220 |         error_msg = f"Error executing {name}: {str(e)}"
221 |         logger.error(error_msg)
222 |         return [types.TextContent(
223 |             type="text",
224 |             text=error_msg
225 |         )]
226 | 
227 | @server.list_resources()
228 | async def handle_list_resources() -> list[types.Resource]:
229 |     return []
230 | 
231 | @server.list_prompts()
232 | async def handle_list_prompts() -> list[types.Prompt]:
233 |     return []
234 | 
235 | async def run():
236 |     global calendar_service
237 |     
238 |     try:
239 |         # Initialize calendar service
240 |         logger.info("Starting Calendar Server...")
241 |         script_dir = os.path.dirname(os.path.abspath(__file__))
242 |         calendar_service = CalendarService(
243 |             credentials_path=os.path.join(script_dir, "..", "credentials", "credentials.json"),
244 |             token_path=os.path.join(script_dir, "..", "credentials", "token.json")
245 |         )
246 |         
247 |         # Authentication with longer timeout
248 |         try:
249 |             await asyncio.wait_for(calendar_service.authenticate(), timeout=60)
250 |             logger.info("Authentication successful")
251 |         except asyncio.TimeoutError:
252 |             logger.error("Authentication timed out")
253 |             raise
254 |         
255 |         # Run the server
256 |         logger.info("Calendar Server is ready")
257 |         async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
258 |             await server.run(
259 |                 read_stream,
260 |                 write_stream,
261 |                 InitializationOptions(
262 |                     server_name="gcalendar",
263 |                     server_version="0.1.0",
264 |                     capabilities=server.get_capabilities(
265 |                         notification_options=NotificationOptions(),
266 |                         experimental_capabilities={},
267 |                     )
268 |                 )
269 |             )
270 |     except ConnectionError as e:
271 |         logger.error(f"Connection error: {str(e)}")
272 |         raise
273 |     except IOError as e:
274 |         logger.error(f"IO error: {str(e)}")
275 |         raise
276 |     except Exception as e:
277 |         logger.error(f"Error in run: {str(e)}")
278 |         raise
279 |     finally:
280 |         if calendar_service:
281 |             logger.info("Closing calendar service...")
282 |             calendar_service.close()
283 | 
284 | async def main():
285 |     try:
286 |         await run()
287 |     except KeyboardInterrupt:
288 |         logger.info("Server shutdown requested")
289 |     except Exception as e:
290 |         logger.error(f"Unexpected error: {str(e)}")
291 |     finally:
292 |         if calendar_service:
293 |             calendar_service.close()
294 | 
295 | if __name__ == "__main__":
296 |     try:
297 |         asyncio.run(main())
298 |     except KeyboardInterrupt:
299 |         pass  # Handle graceful shutdown
```

--------------------------------------------------------------------------------
/src/mcp_server.py:
--------------------------------------------------------------------------------

```python
  1 | from mcp.server import Server, NotificationOptions
  2 | from mcp.server.models import InitializationOptions
  3 | import mcp.server.stdio
  4 | import mcp.types as types
  5 | import logging
  6 | import asyncio
  7 | from calendar_service import CalendarService
  8 | import os
  9 | import signal
 10 | from datetime import datetime
 11 | 
 12 | # Set up logging
 13 | logging.basicConfig(
 14 |     level=logging.INFO,
 15 |     format='[%(asctime)s] %(levelname)s: %(message)s',
 16 |     datefmt='%Y-%m-%d %H:%M:%S'
 17 | )
 18 | logger = logging.getLogger(__name__)
 19 | 
 20 | # Create a server instance
 21 | server = Server("gcalendar-server")
 22 | calendar_service = None
 23 | 
 24 | # Signal handler for graceful shutdown
 25 | def signal_handler(signum, frame):
 26 |     logger.info(f"Received signal {signum}")
 27 |     if calendar_service:
 28 |         calendar_service.close()
 29 |     raise KeyboardInterrupt
 30 | 
 31 | signal.signal(signal.SIGINT, signal_handler)
 32 | signal.signal(signal.SIGTERM, signal_handler)
 33 | 
 34 | @server.list_tools()
 35 | async def handle_list_tools() -> list[types.Tool]:
 36 |     return [
 37 |         types.Tool(
 38 |             name="list",
 39 |             description="List calendar events",
 40 |             arguments={},
 41 |             inputSchema={
 42 |                 "type": "object",
 43 |                 "properties": {}
 44 |             }
 45 |         ),
 46 |         types.Tool(
 47 |             name="create-event",
 48 |             description="Create a new calendar event",
 49 |             arguments={},
 50 |             inputSchema={
 51 |                 "type": "object",
 52 |                 "properties": {
 53 |                     "summary": {
 54 |                         "type": "string",
 55 |                         "description": "Event title"
 56 |                     },
 57 |                     "start_time": {
 58 |                         "type": "string",
 59 |                         "description": "Start time in YYYY-MM-DDTHH:MM:SS format or date in YYYY-MM-DD format"
 60 |                     },
 61 |                     "end_time": {
 62 |                         "type": "string",
 63 |                         "description": "End time (optional). If not provided, event will be 1 hour long"
 64 |                     },
 65 |                     "description": {
 66 |                         "type": "string",
 67 |                         "description": "Event description (optional)"
 68 |                     }
 69 |                 },
 70 |                 "required": ["summary", "start_time"]
 71 |             }
 72 |         ),
 73 |         types.Tool(
 74 |             name="delete-duplicates",
 75 |             description="Delete duplicate events on a specific date",
 76 |             arguments={},
 77 |             inputSchema={
 78 |                 "type": "object",
 79 |                 "properties": {
 80 |                     "target_date": {
 81 |                         "type": "string",
 82 |                         "description": "Target date in YYYY-MM-DD format"
 83 |                     },
 84 |                     "event_summary": {
 85 |                         "type": "string",
 86 |                         "description": "Event title to match"
 87 |                     }
 88 |                 },
 89 |                 "required": ["target_date", "event_summary"]
 90 |             }
 91 |         ),
 92 |         types.Tool(
 93 |             name="delete-event",
 94 |             description="Delete a single calendar event",
 95 |             arguments={},
 96 |             inputSchema={
 97 |                 "type": "object",
 98 |                 "properties": {
 99 |                     "event_time": {
100 |                         "type": "string",
101 |                         "description": "Event time from list output"
102 |                     },
103 |                     "event_summary": {
104 |                         "type": "string",
105 |                         "description": "Event title to match"
106 |                     }
107 |                 },
108 |                 "required": ["event_time", "event_summary"]
109 |             }
110 |         )
111 |     ]
112 | 
113 | @server.call_tool()
114 | async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent]:
115 |     try:
116 |         if name == "list":
117 |             formatted_text = await asyncio.wait_for(
118 |                 calendar_service.list_events(),
119 |                 timeout=60
120 |             )
121 |             return [types.TextContent(
122 |                 type="text",
123 |                 text=formatted_text
124 |             )]
125 |             
126 |         elif name == "create-event":
127 |             if not arguments.get("summary"):
128 |                 return [types.TextContent(
129 |                     type="text",
130 |                     text="Error: Event title (summary) is required"
131 |                 )]
132 |             
133 |             if not arguments.get("start_time"):
134 |                 return [types.TextContent(
135 |                     type="text",
136 |                     text="Error: Start time is required"
137 |                 )]
138 |             
139 |             result = await asyncio.wait_for(
140 |                 calendar_service.create_event(
141 |                     summary=arguments["summary"],
142 |                     start_time=arguments["start_time"],
143 |                     end_time=arguments.get("end_time"),
144 |                     description=arguments.get("description")
145 |                 ),
146 |                 timeout=60
147 |             )
148 |             
149 |             return [types.TextContent(
150 |                 type="text",
151 |                 text=result
152 |             )]
153 |             
154 |         elif name == "delete-duplicates":
155 |             if not arguments.get("target_date"):
156 |                 return [types.TextContent(
157 |                     type="text",
158 |                     text="Error: Target date is required"
159 |                 )]
160 |             
161 |             if not arguments.get("event_summary"):
162 |                 return [types.TextContent(
163 |                     type="text",
164 |                     text="Error: Event title is required"
165 |                 )]
166 |             
167 |             result = await asyncio.wait_for(
168 |                 calendar_service.delete_duplicate_events(
169 |                     target_date=arguments["target_date"],
170 |                     event_summary=arguments["event_summary"]
171 |                 ),
172 |                 timeout=60
173 |             )
174 |             
175 |             return [types.TextContent(
176 |                 type="text",
177 |                 text=result
178 |             )]
179 | 
180 |         elif name == "delete-event":
181 |             if not arguments.get("event_time"):
182 |                 return [types.TextContent(
183 |                     type="text",
184 |                     text="Error: Event time is required"
185 |                 )]
186 |             
187 |             if not arguments.get("event_summary"):
188 |                 return [types.TextContent(
189 |                     type="text",
190 |                     text="Error: Event title is required"
191 |                 )]
192 |             
193 |             result = await asyncio.wait_for(
194 |                 calendar_service.delete_single_event(
195 |                     event_time=arguments["event_time"],
196 |                     event_summary=arguments["event_summary"]
197 |                 ),
198 |                 timeout=60
199 |             )
200 |             
201 |             return [types.TextContent(
202 |                 type="text",
203 |                 text=result
204 |             )]
205 |         
206 |         else:
207 |             return [types.TextContent(
208 |                 type="text",
209 |                 text=f"Unknown tool: {name}"
210 |             )]
211 |             
212 |     except asyncio.TimeoutError:
213 |         error_msg = f"Operation timed out while executing {name}"
214 |         logger.error(error_msg)
215 |         return [types.TextContent(
216 |             type="text",
217 |             text=error_msg
218 |         )]
219 |     except Exception as e:
220 |         error_msg = f"Error executing {name}: {str(e)}"
221 |         logger.error(error_msg)
222 |         return [types.TextContent(
223 |             type="text",
224 |             text=error_msg
225 |         )]
226 | 
227 | @server.list_resources()
228 | async def handle_list_resources() -> list[types.Resource]:
229 |     return []
230 | 
231 | @server.list_prompts()
232 | async def handle_list_prompts() -> list[types.Prompt]:
233 |     return []
234 | 
235 | async def run():
236 |     global calendar_service
237 |     
238 |     try:
239 |         # Initialize calendar service
240 |         logger.info("Starting Calendar Server...")
241 |         script_dir = os.path.dirname(os.path.abspath(__file__))
242 |         calendar_service = CalendarService(
243 |             credentials_path=os.path.join(script_dir, "..", "credentials", "credentials.json"),
244 |             token_path=os.path.join(script_dir, "..", "credentials", "token.json")
245 |         )
246 |         
247 |         # Authentication with longer timeout
248 |         try:
249 |             await asyncio.wait_for(calendar_service.authenticate(), timeout=60)
250 |             logger.info("Authentication successful")
251 |         except asyncio.TimeoutError:
252 |             logger.error("Authentication timed out")
253 |             raise
254 |         
255 |         # Run the server
256 |         logger.info("Calendar Server is ready")
257 |         async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
258 |             await server.run(
259 |                 read_stream,
260 |                 write_stream,
261 |                 InitializationOptions(
262 |                     server_name="gcalendar",
263 |                     server_version="0.1.0",
264 |                     capabilities=server.get_capabilities(
265 |                         notification_options=NotificationOptions(),
266 |                         experimental_capabilities={},
267 |                     )
268 |                 )
269 |             )
270 |     except ConnectionError as e:
271 |         logger.error(f"Connection error: {str(e)}")
272 |         raise
273 |     except IOError as e:
274 |         logger.error(f"IO error: {str(e)}")
275 |         raise
276 |     except Exception as e:
277 |         logger.error(f"Error in run: {str(e)}")
278 |         raise
279 |     finally:
280 |         if calendar_service:
281 |             logger.info("Closing calendar service...")
282 |             calendar_service.close()
283 | 
284 | async def main():
285 |     try:
286 |         await run()
287 |     except KeyboardInterrupt:
288 |         logger.info("Server shutdown requested")
289 |     except Exception as e:
290 |         logger.error(f"Unexpected error: {str(e)}")
291 |     finally:
292 |         if calendar_service:
293 |             calendar_service.close()
294 | 
295 | if __name__ == "__main__":
296 |     try:
297 |         asyncio.run(main())
298 |     except KeyboardInterrupt:
299 |         pass  # Handle graceful shutdown
```

--------------------------------------------------------------------------------
/src/calendar_service.py:
--------------------------------------------------------------------------------

```python
  1 | from google.oauth2.credentials import Credentials
  2 | from google.auth.transport.requests import Request
  3 | from googleapiclient.discovery import build
  4 | from googleapiclient.errors import HttpError
  5 | from datetime import datetime, timedelta
  6 | import logging
  7 | import asyncio
  8 | import os
  9 | import json
 10 | from zoneinfo import ZoneInfo
 11 | 
 12 | # Set up logging
 13 | log_formatter = logging.Formatter(
 14 |     '[%(asctime)s] %(levelname)s: %(message)s',
 15 |     datefmt='%Y-%m-%d %H:%M:%S'
 16 | )
 17 | 
 18 | # Console handler
 19 | console_handler = logging.StreamHandler()
 20 | console_handler.setFormatter(log_formatter)
 21 | 
 22 | # File handler
 23 | log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'GCalendar/logs', 'calendar_service.log')
 24 | # Create logs directory if it doesn't exist
 25 | log_dir = os.path.dirname(log_file)
 26 | os.makedirs(log_dir, exist_ok=True)
 27 | 
 28 | file_handler = logging.FileHandler(log_file, mode='a', encoding='utf-8')
 29 | file_handler.setFormatter(log_formatter)
 30 | 
 31 | # Set up logger
 32 | logger = logging.getLogger(__name__)
 33 | logger.setLevel(logging.INFO)
 34 | logger.addHandler(console_handler)
 35 | logger.addHandler(file_handler)
 36 | 
 37 | class CalendarService:
 38 |     SCOPES = ['https://www.googleapis.com/auth/calendar']
 39 |     TIMEZONE = 'Asia/Bangkok'
 40 |     
 41 |     def __init__(self, credentials_path: str, token_path: str):
 42 |         self.credentials_path = credentials_path
 43 |         self.token_path = token_path
 44 |         self.creds = None
 45 |         self.service = None
 46 |         self.tz = ZoneInfo(self.TIMEZONE)
 47 |         self.events_cache = {}  # Initialize events cache
 48 | 
 49 |     async def authenticate(self):
 50 |         """Authenticate with Google Calendar API"""
 51 |         try:
 52 |             self.creds = Credentials.from_authorized_user_file(self.token_path, self.SCOPES)
 53 |             
 54 |             # Check if credentials are expired and refresh if needed
 55 |             if self.creds and self.creds.expired and self.creds.refresh_token:
 56 |                 logger.info("Token expired, refreshing...")
 57 |                 self.creds.refresh(Request())
 58 |                 # Save refreshed credentials
 59 |                 with open(self.token_path, 'w') as token:
 60 |                     token.write(self.creds.to_json())
 61 |                 logger.info("Token refreshed and saved")
 62 |                     
 63 |             self.service = build('calendar', 'v3', credentials=self.creds)
 64 |             logger.info("Authentication successful")
 65 |             return True
 66 |         except Exception as e:
 67 |             logger.error(f"Authentication error: {str(e)}")
 68 |             raise
 69 | 
 70 |     async def list_events(self, max_results: int = 1000):
 71 |         """List calendar events and cache their IDs"""
 72 |         try:
 73 |             if not self.service:
 74 |                 await self.authenticate()
 75 | 
 76 |             logger.info("Fetching calendar events...")
 77 |             
 78 |             now = datetime.now(self.tz)
 79 |             two_years_ago = now - timedelta(days=730)
 80 |             one_year_later = now + timedelta(days=365)
 81 |             
 82 |             events_result = await asyncio.get_event_loop().run_in_executor(
 83 |                 None,
 84 |                 lambda: self.service.events().list(
 85 |                     calendarId='primary',
 86 |                     timeMin=two_years_ago.isoformat(),
 87 |                     timeMax=one_year_later.isoformat(),
 88 |                     maxResults=max_results,
 89 |                     singleEvents=True,
 90 |                     orderBy='startTime',
 91 |                     timeZone=self.TIMEZONE
 92 |                 ).execute()
 93 |             )
 94 |             
 95 |             events = events_result.get('items', [])
 96 |             logger.info(f"Found {len(events)} events")
 97 | 
 98 |             if not events:
 99 |                 return "No events found."
100 | 
101 |             # Reset and update cache
102 |             self.events_cache = {}
103 |             formatted_text = ""
104 |             
105 |             for event in events:
106 |                 start_time = self._format_event_time(event)
107 |                 summary = event.get('summary', 'No title')
108 |                 cache_key = f"{start_time} {summary}"
109 |                 
110 |                 formatted_text += f"{cache_key}\n"
111 |                 self.events_cache[cache_key] = event['id']
112 |             
113 |             return formatted_text
114 | 
115 |         except HttpError as error:
116 |             error_msg = f"An error occurred: {str(error)}"
117 |             logger.error(error_msg)
118 |             return error_msg
119 | 
120 |     def _format_event_time(self, event: dict) -> str:
121 |         """Format event time consistently with timezone"""
122 |         start = event['start'].get('dateTime', event['start'].get('date'))
123 |         if 'T' in start:  # This is a datetime
124 |             dt = datetime.fromisoformat(start)
125 |             if dt.tzinfo is None:  # Add timezone if not present
126 |                 dt = dt.replace(tzinfo=self.tz)
127 |             formatted_time = dt.strftime('%Y-%m-%dT%H:%M:%S%z')
128 |         else:  # This is a date
129 |             formatted_time = start
130 |         return formatted_time
131 | 
132 |     async def delete_single_event(self, event_time: str, event_summary: str):
133 |         """
134 |         Delete a specific event by its time and summary
135 |         
136 |         Parameters:
137 |         - event_time: Event time in format matching list output
138 |         - event_summary: Event title to match
139 |         
140 |         Returns:
141 |         - Status message
142 |         """
143 |         try:
144 |             if not self.service:
145 |                 await self.authenticate()
146 | 
147 |             # First update the cache
148 |             await self.list_events()
149 |             
150 |             # Get event ID from cache
151 |             cache_key = f"{event_time} {event_summary}"
152 |             event_id = self.events_cache.get(cache_key)
153 |             
154 |             if not event_id:
155 |                 return f"Event not found: {cache_key}"
156 |             
157 |             # Delete the event
158 |             await asyncio.get_event_loop().run_in_executor(
159 |                 None,
160 |                 lambda: self.service.events().delete(
161 |                     calendarId='primary',
162 |                     eventId=event_id
163 |                 ).execute()
164 |             )
165 |             
166 |             logger.info(f"Deleted event: {event_id} - {cache_key}")
167 |             return f"Successfully deleted event: {cache_key}"
168 | 
169 |         except HttpError as error:
170 |             error_msg = f"An error occurred: {str(error)}"
171 |             logger.error(error_msg)
172 |             return error_msg
173 |         except Exception as error:
174 |             error_msg = f"Unexpected error: {str(error)}"
175 |             logger.error(error_msg)
176 |             return error_msg
177 | 
178 |     async def create_event(self, summary: str, start_time: str, end_time: str = None, description: str = None):
179 |         """
180 |         Create a new calendar event
181 |         
182 |         Parameters:
183 |         - summary: Event title
184 |         - start_time: Start time in YYYY-MM-DDTHH:MM:SS format or date in YYYY-MM-DD format
185 |         - end_time: End time (optional). If not provided, event will be 1 hour long
186 |         - description: Event description (optional)
187 |         
188 |         Returns:
189 |         - Status message
190 |         """
191 |         try:
192 |             if not self.service:
193 |                 await self.authenticate()
194 | 
195 |             # Parse start time
196 |             try:
197 |                 # Check if time is included
198 |                 if 'T' in start_time:
199 |                     start_dt = datetime.fromisoformat(start_time)
200 |                     is_datetime = True
201 |                 else:
202 |                     start_dt = datetime.strptime(start_time, '%Y-%m-%d')
203 |                     is_datetime = False
204 |             except ValueError:
205 |                 return f"Invalid start time format: {start_time}. Use YYYY-MM-DDTHH:MM:SS or YYYY-MM-DD"
206 | 
207 |             # Handle end time
208 |             if end_time:
209 |                 try:
210 |                     if 'T' in end_time:
211 |                         end_dt = datetime.fromisoformat(end_time)
212 |                     else:
213 |                         end_dt = datetime.strptime(end_time, '%Y-%m-%d')
214 |                         if is_datetime:
215 |                             # If start has time but end doesn't, make end time 23:59:59
216 |                             end_dt = end_dt.replace(hour=23, minute=59, second=59)
217 |                 except ValueError:
218 |                     return f"Invalid end time format: {end_time}. Use YYYY-MM-DDTHH:MM:SS or YYYY-MM-DD"
219 |             else:
220 |                 # Default to 1 hour duration for datetime events, or same day for date events
221 |                 if is_datetime:
222 |                     end_dt = start_dt + timedelta(hours=1)
223 |                 else:
224 |                     end_dt = start_dt
225 | 
226 |             # Create event body
227 |             event_body = {
228 |                 'summary': summary,
229 |                 'start': {
230 |                     'dateTime' if is_datetime else 'date': start_dt.isoformat(),
231 |                     'timeZone': self.TIMEZONE if is_datetime else None
232 |                 },
233 |                 'end': {
234 |                     'dateTime' if is_datetime else 'date': end_dt.isoformat(),
235 |                     'timeZone': self.TIMEZONE if is_datetime else None
236 |                 }
237 |             }
238 | 
239 |             # Add optional description if provided
240 |             if description:
241 |                 event_body['description'] = description
242 | 
243 |             # Create the event
244 |             created_event = await asyncio.get_event_loop().run_in_executor(
245 |                 None,
246 |                 lambda: self.service.events().insert(
247 |                     calendarId='primary',
248 |                     body=event_body
249 |                 ).execute()
250 |             )
251 |             
252 |             logger.info(f"Created event: {created_event['id']} - {summary}")
253 |             return f"Successfully created event: {summary}"
254 | 
255 |         except HttpError as error:
256 |             error_msg = f"An error occurred: {str(error)}"
257 |             logger.error(error_msg)
258 |             return error_msg
259 |         except Exception as error:
260 |             error_msg = f"Unexpected error: {str(error)}"
261 |             logger.error(error_msg)
262 |             return error_msg
263 | 
264 |     async def delete_duplicate_events(self, target_date: str, event_summary: str):
265 |         """
266 |         Delete duplicate events on a specific date
267 |         
268 |         Parameters:
269 |         - target_date: Target date in YYYY-MM-DD format
270 |         - event_summary: Event title to match
271 |         
272 |         Returns:
273 |         - Status message
274 |         """
275 |         try:
276 |             if not self.service:
277 |                 await self.authenticate()
278 | 
279 |             # Parse target date
280 |             try:
281 |                 target_dt = datetime.strptime(target_date, '%Y-%m-%d')
282 |             except ValueError:
283 |                 return f"Invalid date format: {target_date}. Use YYYY-MM-DD"
284 | 
285 |             # Set time range for the target date
286 |             start_dt = target_dt.replace(hour=0, minute=0, second=0, tzinfo=self.tz)
287 |             end_dt = target_dt.replace(hour=23, minute=59, second=59, tzinfo=self.tz)
288 | 
289 |             # Get events for the target date
290 |             events_result = await asyncio.get_event_loop().run_in_executor(
291 |                 None,
292 |                 lambda: self.service.events().list(
293 |                     calendarId='primary',
294 |                     timeMin=start_dt.isoformat(),
295 |                     timeMax=end_dt.isoformat(),
296 |                     singleEvents=True,
297 |                     orderBy='startTime',
298 |                     timeZone=self.TIMEZONE
299 |                 ).execute()
300 |             )
301 |             
302 |             events = events_result.get('items', [])
303 |             
304 |             # Find duplicate events
305 |             duplicate_ids = []
306 |             seen_times = set()
307 |             
308 |             for event in events:
309 |                 if event.get('summary') == event_summary:
310 |                     start_time = self._format_event_time(event)
311 |                     if start_time in seen_times:
312 |                         duplicate_ids.append(event['id'])
313 |                     else:
314 |                         seen_times.add(start_time)
315 |             
316 |             if not duplicate_ids:
317 |                 return f"No duplicate events found for '{event_summary}' on {target_date}"
318 | 
319 |             # Delete duplicate events
320 |             deleted_count = 0
321 |             for event_id in duplicate_ids:
322 |                 try:
323 |                     await asyncio.get_event_loop().run_in_executor(
324 |                         None,
325 |                         lambda: self.service.events().delete(
326 |                             calendarId='primary',
327 |                             eventId=event_id
328 |                         ).execute()
329 |                     )
330 |                     deleted_count += 1
331 |                 except HttpError as error:
332 |                     logger.error(f"Error deleting event {event_id}: {str(error)}")
333 | 
334 |             logger.info(f"Deleted {deleted_count} duplicate events for '{event_summary}' on {target_date}")
335 |             return f"Successfully deleted {deleted_count} duplicate events for '{event_summary}' on {target_date}"
336 | 
337 |         except HttpError as error:
338 |             error_msg = f"An error occurred: {str(error)}"
339 |             logger.error(error_msg)
340 |             return error_msg
341 |         except Exception as error:
342 |             error_msg = f"Unexpected error: {str(error)}"
343 |             logger.error(error_msg)
344 |             return error_msg
345 | 
346 |     async def renew_token(self):
347 |         """Renew the authentication token"""
348 |         try:
349 |             if not self.creds:
350 |                 self.creds = Credentials.from_authorized_user_file(self.token_path, self.SCOPES)
351 | 
352 |             if self.creds and self.creds.expired and self.creds.refresh_token:
353 |                 logger.info("Token expired, refreshing...")
354 |                 self.creds.refresh(Request())
355 |                 # Save refreshed credentials
356 |                 with open(self.token_path, 'w') as token:
357 |                     token.write(self.creds.to_json())
358 |                 logger.info("Token refreshed and saved")
359 |                 return "Token renewed successfully"
360 |             else:
361 |                 return "Token is still valid"
362 |         except Exception as e:
363 |             error_msg = f"Error renewing token: {str(e)}"
364 |             logger.error(error_msg)
365 |             return error_msg
366 | 
367 |     def close(self):
368 |         """Clean up resources"""
369 |         if self.service:
370 |             logger.info("Closing calendar service")
371 |             self.service.close()
```

--------------------------------------------------------------------------------
/GCalendar/src/calendar_service.py:
--------------------------------------------------------------------------------

```python
  1 | from google.oauth2.credentials import Credentials
  2 | from google.auth.transport.requests import Request
  3 | from googleapiclient.discovery import build
  4 | from googleapiclient.errors import HttpError
  5 | from datetime import datetime, timedelta
  6 | import logging
  7 | import asyncio
  8 | import os
  9 | import json
 10 | from zoneinfo import ZoneInfo
 11 | import sys
 12 | 
 13 | # Set up logging
 14 | log_formatter = logging.Formatter(
 15 |     '[%(asctime)s] %(levelname)s: %(message)s',
 16 |     datefmt='%Y-%m-%d %H:%M:%S'
 17 | )
 18 | 
 19 | # Console handler
 20 | console_handler = logging.StreamHandler()
 21 | console_handler.setFormatter(log_formatter)
 22 | 
 23 | # File handler
 24 | log_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
 25 | os.makedirs(log_dir, exist_ok=True)  # Create logs directory if it doesn't exist
 26 | log_file = os.path.join(log_dir, 'calendar_service.log')
 27 | file_handler = logging.FileHandler(log_file, mode='a', encoding='utf-8')
 28 | file_handler.setFormatter(log_formatter)
 29 | 
 30 | # Set up logger
 31 | logger = logging.getLogger(__name__)
 32 | logger.setLevel(logging.INFO)
 33 | logger.addHandler(console_handler)
 34 | logger.addHandler(file_handler)
 35 | 
 36 | class CalendarService:
 37 |     SCOPES = ['https://www.googleapis.com/auth/calendar']
 38 |     # Try to use Bangkok timezone, but fallback to others if not available
 39 |     TIMEZONE = 'Asia/Bangkok'
 40 |     
 41 |     @classmethod
 42 |     def get_available_timezone(cls):
 43 |         """Get an available timezone closest to Bangkok, with fallbacks"""
 44 |         timezones_to_try = [
 45 |             'Asia/Bangkok',      # First choice (UTC+7)
 46 |             'Asia/Jakarta',      # Second choice (UTC+7)
 47 |             'Asia/Singapore',    # Third choice (UTC+8)
 48 |             'Asia/Kolkata',      # Fourth choice (UTC+5:30)
 49 |             'UTC'                # Last resort
 50 |         ]
 51 |         
 52 |         for tz in timezones_to_try:
 53 |             try:
 54 |                 ZoneInfo(tz)
 55 |                 logger.info(f"Using timezone: {tz}")
 56 |                 return tz
 57 |             except Exception as e:
 58 |                 logger.warning(f"Timezone {tz} not available: {str(e)}")
 59 |         
 60 |         logger.error("No suitable timezone found. Please install tzdata package with: pip install tzdata")
 61 |         sys.exit(1)
 62 |     
 63 |     def __init__(self, credentials_path: str, token_path: str):
 64 |         self.credentials_path = credentials_path
 65 |         self.token_path = token_path
 66 |         self.creds = None
 67 |         self.service = None
 68 |         # Use the first available timezone
 69 |         self.TIMEZONE = self.get_available_timezone()
 70 |         self.tz = ZoneInfo(self.TIMEZONE)
 71 |         self.events_cache = {}  # Initialize events cache
 72 | 
 73 |     async def authenticate(self):
 74 |         """Authenticate with Google Calendar API"""
 75 |         try:
 76 |             self.creds = Credentials.from_authorized_user_file(self.token_path, self.SCOPES)
 77 |             
 78 |             # Check if credentials are expired and refresh if needed
 79 |             if self.creds and self.creds.expired and self.creds.refresh_token:
 80 |                 logger.info("Token expired, refreshing...")
 81 |                 self.creds.refresh(Request())
 82 |                 # Save refreshed credentials
 83 |                 with open(self.token_path, 'w') as token:
 84 |                     token.write(self.creds.to_json())
 85 |                 logger.info("Token refreshed and saved")
 86 |                     
 87 |             self.service = build('calendar', 'v3', credentials=self.creds)
 88 |             logger.info("Authentication successful")
 89 |             return True
 90 |         except Exception as e:
 91 |             logger.error(f"Authentication error: {str(e)}")
 92 |             raise
 93 | 
 94 |     async def list_events(self, max_results: int = 1000):
 95 |         """List calendar events and cache their IDs"""
 96 |         try:
 97 |             if not self.service:
 98 |                 await self.authenticate()
 99 | 
100 |             logger.info("Fetching calendar events...")
101 |             
102 |             now = datetime.now(self.tz)
103 |             two_years_ago = now - timedelta(days=730)
104 |             one_year_later = now + timedelta(days=365)
105 |             
106 |             events_result = await asyncio.get_event_loop().run_in_executor(
107 |                 None,
108 |                 lambda: self.service.events().list(
109 |                     calendarId='primary',
110 |                     timeMin=two_years_ago.isoformat(),
111 |                     timeMax=one_year_later.isoformat(),
112 |                     maxResults=max_results,
113 |                     singleEvents=True,
114 |                     orderBy='startTime',
115 |                     timeZone=self.TIMEZONE
116 |                 ).execute()
117 |             )
118 |             
119 |             events = events_result.get('items', [])
120 |             logger.info(f"Found {len(events)} events")
121 | 
122 |             if not events:
123 |                 return "No events found."
124 | 
125 |             # Reset and update cache
126 |             self.events_cache = {}
127 |             formatted_text = ""
128 |             
129 |             for event in events:
130 |                 start_time = self._format_event_time(event)
131 |                 summary = event.get('summary', 'No title')
132 |                 cache_key = f"{start_time} {summary}"
133 |                 
134 |                 formatted_text += f"{cache_key}\n"
135 |                 self.events_cache[cache_key] = event['id']
136 |             
137 |             return formatted_text
138 | 
139 |         except HttpError as error:
140 |             error_msg = f"An error occurred: {str(error)}"
141 |             logger.error(error_msg)
142 |             return error_msg
143 | 
144 |     def _format_event_time(self, event: dict) -> str:
145 |         """Format event time consistently with timezone"""
146 |         start = event['start'].get('dateTime', event['start'].get('date'))
147 |         if 'T' in start:  # This is a datetime
148 |             dt = datetime.fromisoformat(start)
149 |             if dt.tzinfo is None:  # Add timezone if not present
150 |                 dt = dt.replace(tzinfo=self.tz)
151 |             formatted_time = dt.strftime('%Y-%m-%dT%H:%M:%S%z')
152 |         else:  # This is a date
153 |             formatted_time = start
154 |         return formatted_time
155 | 
156 |     async def delete_single_event(self, event_time: str, event_summary: str):
157 |         """
158 |         Delete a specific event by its time and summary
159 |         
160 |         Parameters:
161 |         - event_time: Event time in format matching list output
162 |         - event_summary: Event title to match
163 |         
164 |         Returns:
165 |         - Status message
166 |         """
167 |         try:
168 |             if not self.service:
169 |                 await self.authenticate()
170 | 
171 |             # First update the cache
172 |             await self.list_events()
173 |             
174 |             # Get event ID from cache
175 |             cache_key = f"{event_time} {event_summary}"
176 |             event_id = self.events_cache.get(cache_key)
177 |             
178 |             if not event_id:
179 |                 return f"Event not found: {cache_key}"
180 |             
181 |             # Delete the event
182 |             await asyncio.get_event_loop().run_in_executor(
183 |                 None,
184 |                 lambda: self.service.events().delete(
185 |                     calendarId='primary',
186 |                     eventId=event_id
187 |                 ).execute()
188 |             )
189 |             
190 |             logger.info(f"Deleted event: {event_id} - {cache_key}")
191 |             return f"Successfully deleted event: {cache_key}"
192 | 
193 |         except HttpError as error:
194 |             error_msg = f"An error occurred: {str(error)}"
195 |             logger.error(error_msg)
196 |             return error_msg
197 |         except Exception as error:
198 |             error_msg = f"Unexpected error: {str(error)}"
199 |             logger.error(error_msg)
200 |             return error_msg
201 | 
202 |     async def create_event(self, summary: str, start_time: str, end_time: str = None, description: str = None):
203 |         """
204 |         Create a new calendar event
205 |         
206 |         Parameters:
207 |         - summary: Event title
208 |         - start_time: Start time in YYYY-MM-DDTHH:MM:SS format or date in YYYY-MM-DD format
209 |         - end_time: End time (optional). If not provided, event will be 1 hour long
210 |         - description: Event description (optional)
211 |         
212 |         Returns:
213 |         - Status message
214 |         """
215 |         try:
216 |             if not self.service:
217 |                 await self.authenticate()
218 | 
219 |             # Parse start time
220 |             try:
221 |                 # Check if time is included
222 |                 if 'T' in start_time:
223 |                     start_dt = datetime.fromisoformat(start_time)
224 |                     is_datetime = True
225 |                 else:
226 |                     start_dt = datetime.strptime(start_time, '%Y-%m-%d')
227 |                     is_datetime = False
228 |             except ValueError:
229 |                 return f"Invalid start time format: {start_time}. Use YYYY-MM-DDTHH:MM:SS or YYYY-MM-DD"
230 | 
231 |             # Handle end time
232 |             if end_time:
233 |                 try:
234 |                     if 'T' in end_time:
235 |                         end_dt = datetime.fromisoformat(end_time)
236 |                     else:
237 |                         end_dt = datetime.strptime(end_time, '%Y-%m-%d')
238 |                         if is_datetime:
239 |                             # If start has time but end doesn't, make end time 23:59:59
240 |                             end_dt = end_dt.replace(hour=23, minute=59, second=59)
241 |                 except ValueError:
242 |                     return f"Invalid end time format: {end_time}. Use YYYY-MM-DDTHH:MM:SS or YYYY-MM-DD"
243 |             else:
244 |                 # Default to 1 hour duration for datetime events, or same day for date events
245 |                 if is_datetime:
246 |                     end_dt = start_dt + timedelta(hours=1)
247 |                 else:
248 |                     end_dt = start_dt
249 | 
250 |             # Create event body
251 |             event_body = {
252 |                 'summary': summary,
253 |                 'start': {
254 |                     'dateTime' if is_datetime else 'date': start_dt.isoformat(),
255 |                     'timeZone': self.TIMEZONE if is_datetime else None
256 |                 },
257 |                 'end': {
258 |                     'dateTime' if is_datetime else 'date': end_dt.isoformat(),
259 |                     'timeZone': self.TIMEZONE if is_datetime else None
260 |                 }
261 |             }
262 | 
263 |             # Add optional description if provided
264 |             if description:
265 |                 event_body['description'] = description
266 | 
267 |             # Create the event
268 |             created_event = await asyncio.get_event_loop().run_in_executor(
269 |                 None,
270 |                 lambda: self.service.events().insert(
271 |                     calendarId='primary',
272 |                     body=event_body
273 |                 ).execute()
274 |             )
275 |             
276 |             logger.info(f"Created event: {created_event['id']} - {summary}")
277 |             return f"Successfully created event: {summary}"
278 | 
279 |         except HttpError as error:
280 |             error_msg = f"An error occurred: {str(error)}"
281 |             logger.error(error_msg)
282 |             return error_msg
283 |         except Exception as error:
284 |             error_msg = f"Unexpected error: {str(error)}"
285 |             logger.error(error_msg)
286 |             return error_msg
287 | 
288 |     async def delete_duplicate_events(self, target_date: str, event_summary: str):
289 |         """
290 |         Delete duplicate events on a specific date
291 |         
292 |         Parameters:
293 |         - target_date: Target date in YYYY-MM-DD format
294 |         - event_summary: Event title to match
295 |         
296 |         Returns:
297 |         - Status message
298 |         """
299 |         try:
300 |             if not self.service:
301 |                 await self.authenticate()
302 | 
303 |             # Parse target date
304 |             try:
305 |                 target_dt = datetime.strptime(target_date, '%Y-%m-%d')
306 |             except ValueError:
307 |                 return f"Invalid date format: {target_date}. Use YYYY-MM-DD"
308 | 
309 |             # Set time range for the target date
310 |             start_dt = target_dt.replace(hour=0, minute=0, second=0, tzinfo=self.tz)
311 |             end_dt = target_dt.replace(hour=23, minute=59, second=59, tzinfo=self.tz)
312 | 
313 |             # Get events for the target date
314 |             events_result = await asyncio.get_event_loop().run_in_executor(
315 |                 None,
316 |                 lambda: self.service.events().list(
317 |                     calendarId='primary',
318 |                     timeMin=start_dt.isoformat(),
319 |                     timeMax=end_dt.isoformat(),
320 |                     singleEvents=True,
321 |                     orderBy='startTime',
322 |                     timeZone=self.TIMEZONE
323 |                 ).execute()
324 |             )
325 |             
326 |             events = events_result.get('items', [])
327 |             
328 |             # Find duplicate events
329 |             duplicate_ids = []
330 |             seen_times = set()
331 |             
332 |             for event in events:
333 |                 if event.get('summary') == event_summary:
334 |                     start_time = self._format_event_time(event)
335 |                     if start_time in seen_times:
336 |                         duplicate_ids.append(event['id'])
337 |                     else:
338 |                         seen_times.add(start_time)
339 |             
340 |             if not duplicate_ids:
341 |                 return f"No duplicate events found for '{event_summary}' on {target_date}"
342 | 
343 |             # Delete duplicate events
344 |             deleted_count = 0
345 |             for event_id in duplicate_ids:
346 |                 try:
347 |                     await asyncio.get_event_loop().run_in_executor(
348 |                         None,
349 |                         lambda: self.service.events().delete(
350 |                             calendarId='primary',
351 |                             eventId=event_id
352 |                         ).execute()
353 |                     )
354 |                     deleted_count += 1
355 |                 except HttpError as error:
356 |                     logger.error(f"Error deleting event {event_id}: {str(error)}")
357 | 
358 |             logger.info(f"Deleted {deleted_count} duplicate events for '{event_summary}' on {target_date}")
359 |             return f"Successfully deleted {deleted_count} duplicate events for '{event_summary}' on {target_date}"
360 | 
361 |         except HttpError as error:
362 |             error_msg = f"An error occurred: {str(error)}"
363 |             logger.error(error_msg)
364 |             return error_msg
365 |         except Exception as error:
366 |             error_msg = f"Unexpected error: {str(error)}"
367 |             logger.error(error_msg)
368 |             return error_msg
369 | 
370 |     async def renew_token(self):
371 |         """Renew the authentication token"""
372 |         try:
373 |             if not self.creds:
374 |                 self.creds = Credentials.from_authorized_user_file(self.token_path, self.SCOPES)
375 | 
376 |             if self.creds and self.creds.expired and self.creds.refresh_token:
377 |                 logger.info("Token expired, refreshing...")
378 |                 self.creds.refresh(Request())
379 |                 # Save refreshed credentials
380 |                 with open(self.token_path, 'w') as token:
381 |                     token.write(self.creds.to_json())
382 |                 logger.info("Token refreshed and saved")
383 |                 return "Token renewed successfully"
384 |             else:
385 |                 return "Token is still valid"
386 |         except Exception as e:
387 |             error_msg = f"Error renewing token: {str(e)}"
388 |             logger.error(error_msg)
389 |             return error_msg
390 | 
391 |     def close(self):
392 |         """Clean up resources"""
393 |         if self.service:
394 |             logger.info("Closing calendar service")
395 |             self.service.close()
```