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

```
├── .env
├── .gitignore
├── docs
│   └── Demo-on-Dive-Desktop.png
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── calendar-tools
│   │   ├── _index.ts
│   │   ├── calendarTools.ts
│   │   ├── createEvent.ts
│   │   ├── deleteEvent.ts
│   │   ├── listEvents.ts
│   │   └── updateEvent.ts
│   ├── index.ts
│   └── utils
│       ├── auth.ts
│       └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------

```
1 | 
```

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

```
1 | dist/
2 | node_modules/
3 | credentials.json
4 | 
```

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

```markdown
 1 | # Calendar Tools MCP Server
 2 | 
 3 | A powerful Model Context Protocol (MCP) server providing comprehensive calendar management capabilities.
 4 | 
 5 | ## Features
 6 | 
 7 | ### Calendar Management
 8 | 
 9 | - Create calendar events
10 | - List calendar events
11 | - Update existing events
12 | - Delete events
13 | 
14 | ## Demo on Dive Desktop
15 | 
16 | ![Calendar Tools Demo](docs/Demo-on-Dive-Desktop.png)
17 | 
18 | ## Installation
19 | 
20 | ### Manual Installation
21 | 
22 | ```bash
23 | npm install -g @cablate/mcp-google-calendar
24 | ```
25 | 
26 | ## Usage
27 | 
28 | ### Cli
29 | 
30 | ```bash
31 | mcp-google-calendar
32 | ```
33 | 
34 | ### With [Dive Desktop](https://github.com/OpenAgentPlatform/Dive)
35 | 
36 | 1. Click "+ Add MCP Server" in Dive Desktop
37 | 2. Copy and paste this configuration:
38 | 
39 | ```json
40 | {
41 |   "mcpServers": {
42 |     "calendar": {
43 |       "command": "npx",
44 |       "args": ["-y", "@cablate/mcp-google-calendar"],
45 |       "env": {
46 |         "GOOGLE_CALENDAR_ID": "your_calendar_id",
47 |         "GOOGLE_TIME_ZONE": "your_time_zone",
48 |         "GOOGLE_CREDENTIALS_PATH": "your_credentials_path"
49 |       },
50 |       "enabled": true
51 |     }
52 |   }
53 | }
54 | ```
55 | 
56 | 3. Click "Save" to install the MCP server
57 | 
58 | ## Google Service Account and Credentials
59 | 
60 | Here is the simple steps to create a google service account and credentials:
61 | 
62 | 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
63 | 2. Create a new project or select an existing project
64 | 3. Navigate to the "IAM & Admin" section
65 | 4. Click on "Service Accounts"
66 | 5. Click on "Create Service Account"
67 | 6. Enter a name for the service account (e.g., "MCP Google Calendar")
68 | 7. Click on "Create"
69 | 8. Click on "Create Key"
70 | 9. Select "JSON" as the key type
71 | 10. Click on "Create"
72 | 11. Download the JSON file and save it as `credentials.json`
73 | 
74 | if still got any question, google and find the answer.
75 | 
76 | ## License
77 | 
78 | MIT
79 | 
80 | ## Contributing
81 | 
82 | Welcome community participation and contributions! Here are ways to contribute:
83 | 
84 | - ⭐️ Star the project if you find it helpful
85 | - 🐛 Submit Issues: Report problems or provide suggestions
86 | - 🔧 Create Pull Requests: Submit code improvements
87 | 
88 | ## Contact
89 | 
90 | If you have any questions or suggestions, feel free to reach out:
91 | 
92 | - 📧 Email: [[email protected]](mailto:[email protected])
93 | - 📧 GitHub: [CabLate](https://github.com/cablate/)
94 | - 🤝 Collaboration: Welcome to discuss project cooperation
95 | - 📚 Technical Guidance: Sincere welcome for suggestions and guidance
96 | 
```

--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------

```typescript
1 | export function convertToTimeZone(dateTime: string, timeZone: string): string {
2 |   return new Date(
3 |     new Date(dateTime).toLocaleString('en-US', {
4 |       timeZone: timeZone
5 |     })
6 |   ).toISOString();
7 | }
```

--------------------------------------------------------------------------------
/src/calendar-tools/_index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { CREATE_EVENT_TOOL, DELETE_EVENT_TOOL, LIST_EVENTS_TOOL, UPDATE_EVENT_TOOL } from "./calendarTools.js";
 2 | 
 3 | 
 4 | export const tools = [CREATE_EVENT_TOOL, LIST_EVENTS_TOOL, UPDATE_EVENT_TOOL, DELETE_EVENT_TOOL];
 5 | 
 6 | export * from "./calendarTools.js";
 7 | export * from "./createEvent.js";
 8 | export * from "./deleteEvent.js";
 9 | export * from "./listEvents.js";
10 | export * from "./updateEvent.js";
11 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "strict": true,
 5 |     "esModuleInterop": true,
 6 |     "skipLibCheck": true,
 7 |     "forceConsistentCasingInFileNames": true,
 8 |     "resolveJsonModule": true,
 9 |     "outDir": "./dist",
10 |     "rootDir": "./src",
11 |     "moduleResolution": "NodeNext",
12 |     "module": "NodeNext",
13 |     "noImplicitAny": false
14 |   },
15 |   "exclude": ["node_modules"],
16 |   "include": ["src/**/*"]
17 | }
```

--------------------------------------------------------------------------------
/src/calendar-tools/deleteEvent.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { google } from "googleapis";
 2 | import { getAuthClient } from "../utils/auth.js";
 3 | 
 4 | export async function deleteEvent(eventId: string) {
 5 |   try {
 6 |     const auth = await getAuthClient();
 7 |     const calendar = google.calendar({ version: "v3", auth });
 8 | 
 9 |     await calendar.events.delete({
10 |       calendarId: process.env.GOOGLE_CALENDAR_ID || "primary",
11 |       eventId: eventId,
12 |       sendUpdates: "all",
13 |     });
14 | 
15 |     return {
16 |       success: true,
17 |       data: "Event successfully deleted",
18 |     };
19 |   } catch (error) {
20 |     return {
21 |       success: false,
22 |       error: error instanceof Error ? error.message : "Error deleting event",
23 |     };
24 |   }
25 | }
26 | 
```

--------------------------------------------------------------------------------
/src/utils/auth.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { promises as fs } from "fs";
 2 | import { JWT } from "google-auth-library";
 3 | import path from "path";
 4 | 
 5 | const SCOPES = ["https://www.googleapis.com/auth/calendar"];
 6 | const CREDENTIALS_PATH = process.env.GOOGLE_CREDENTIALS_PATH || path.join(process.cwd(), "credentials.json");
 7 | 
 8 | export async function getAuthClient(): Promise<JWT> {
 9 |   try {
10 |     const content = await fs.readFile(CREDENTIALS_PATH);
11 |     const credentials = JSON.parse(content.toString());
12 | 
13 |     const client = new JWT({
14 |       email: credentials.client_email,
15 |       key: credentials.private_key,
16 |       scopes: SCOPES,
17 |     });
18 | 
19 |     return client;
20 |   } catch (error) {
21 |     console.error("Authentication error:", error);
22 |     throw error;
23 |   }
24 | }
25 | 
```

--------------------------------------------------------------------------------
/src/calendar-tools/listEvents.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { calendar_v3, google } from "googleapis";
 2 | import { getAuthClient } from "../utils/auth.js";
 3 | import { convertToTimeZone } from "../utils/index.js";
 4 | 
 5 | interface ListEventsParams {
 6 |   timeMin: string;
 7 |   maxResults: number;
 8 | }
 9 | 
10 | interface ListEventsResult {
11 |   success: boolean;
12 |   error?: string;
13 |   data?: calendar_v3.Schema$Event[];
14 | }
15 | 
16 | export async function listEvents(params: ListEventsParams): Promise<ListEventsResult> {
17 |   try {
18 |     const auth = await getAuthClient();
19 |     const calendar = google.calendar({ version: "v3", auth });
20 | 
21 |     const result = await calendar.events.list({
22 |       calendarId: process.env.GOOGLE_CALENDAR_ID || "primary",
23 |       timeMin: convertToTimeZone(params.timeMin, process.env.GOOGLE_TIME_ZONE || "Asia/Taipei"),
24 |       maxResults: params.maxResults,
25 |       singleEvents: true,
26 |       orderBy: "startTime",
27 |     });
28 | 
29 |     return {
30 |       success: true,
31 |       data: result.data?.items || [],
32 |     };
33 |   } catch (error) {
34 |     return {
35 |       success: false,
36 |       error: error instanceof Error ? error.message : "Error getting event list",
37 |     };
38 |   }
39 | }
40 | 
```

--------------------------------------------------------------------------------
/src/calendar-tools/createEvent.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { calendar_v3, google } from "googleapis";
 2 | import { getAuthClient } from "../utils/auth.js";
 3 | 
 4 | interface CreateEventParams {
 5 |   summary: string;
 6 |   description?: string;
 7 |   startTime: string;
 8 |   endTime: string;
 9 |   attendees?: string[];
10 | }
11 | 
12 | interface CreateEventResult {
13 |   success: boolean;
14 |   error?: string;
15 |   data?: calendar_v3.Schema$Event;
16 | }
17 | 
18 | export async function createEvent(params: CreateEventParams): Promise<CreateEventResult> {
19 |   try {
20 |     const auth = await getAuthClient();
21 |     const calendar = google.calendar({ version: "v3", auth });
22 | 
23 |     const event = {
24 |       summary: params.summary,
25 |       description: params.description,
26 |       start: {
27 |         dateTime: params.startTime,
28 |         timeZone: process.env.GOOGLE_TIME_ZONE || "Etc/UTC",
29 |       },
30 |       end: {
31 |         dateTime: params.endTime,
32 |         timeZone: process.env.GOOGLE_TIME_ZONE || "Etc/UTC",
33 |       },
34 |       attendees: params.attendees?.map((email) => ({ email })),
35 |     };
36 | 
37 |     const result = await calendar.events.insert({
38 |       calendarId: process.env.GOOGLE_CALENDAR_ID || "primary",
39 |       requestBody: event,
40 |       sendUpdates: "all",
41 |     });
42 | 
43 |     return {
44 |       success: true,
45 |       data: result.data || undefined,
46 |     };
47 |   } catch (error) {
48 |     return {
49 |       success: false,
50 |       error: error instanceof Error ? error.message : "Error creating event",
51 |     };
52 |   }
53 | }
54 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@cablate/mcp-google-calendar",
 3 |   "version": "0.0.3",
 4 |   "type": "module",
 5 |   "description": "MCP server that provides google calendar capabilities",
 6 |   "main": "dist/index.cjs",
 7 |   "license": "MIT",  
 8 |   "scripts": {
 9 |     "build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.cjs --external:pdfreader --external:jsdom --external:mammoth --external:csv-parse --external:libreoffice-convert && shx chmod +x dist/index.cjs",
10 |     "dev": "ts-node src/index.ts",
11 |     "start": "node dist/index.cjs"
12 |   },
13 |   "dependencies": {
14 |     "@google-cloud/local-auth": "^2.1.0",
15 |     "@modelcontextprotocol/sdk": "^1.5.0",
16 |     "esbuild": "^0.25.0",
17 |     "googleapis": "^105.0.0",
18 |     "shx": "^0.3.4",
19 |     "typescript": "^5.3.3"
20 |   },
21 |   "devDependencies": {
22 |     "@types/node": "^20.11.16",
23 |     "ts-node": "^10.9.2"
24 |   },
25 |   "author": "CabLate",
26 |   "files": [
27 |     "dist",
28 |     "dist/**/*.map",
29 |     "README.md"
30 |   ],
31 |   "bin": {
32 |     "mcp-doc-forge": "./dist/index.cjs"
33 |   },
34 |   "keywords": [
35 |     "mcp",
36 |     "mcp-server",
37 |     "google-calendar",
38 |     "google-api",
39 |     "calendar",
40 |     "ai",
41 |     "dive"
42 |   ],
43 |   "homepage": "https://github.com/cablate/mcp-google-calendar#readme",
44 |   "repository": {
45 |     "type": "git",
46 |     "url": "git+https://github.com/cablate/mcp-google-calendar.git"
47 |   },
48 |   "bugs": {
49 |     "url": "https://github.com/cablate/mcp-google-calendar/issues"
50 |   }
51 | }
52 | 
```

--------------------------------------------------------------------------------
/src/calendar-tools/updateEvent.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { google } from "googleapis";
 2 | import { getAuthClient } from "../utils/auth.js";
 3 | import { convertToTimeZone } from "../utils/index.js";
 4 | 
 5 | interface UpdateEventParams {
 6 |   summary?: string;
 7 |   description?: string;
 8 |   startTime?: string;
 9 |   endTime?: string;
10 |   attendees?: string[];
11 | }
12 | 
13 | export async function updateEvent(eventId: string, updates: UpdateEventParams) {
14 |   try {
15 |     const auth = await getAuthClient();
16 |     const calendar = google.calendar({ version: "v3", auth });
17 | 
18 |     // 先取得現有活動資料
19 |     const event = await calendar.events.get({
20 |       calendarId: process.env.GOOGLE_CALENDAR_ID || "primary",
21 |       eventId: eventId,
22 |     });
23 | 
24 |     // 準備更新的資料
25 |     const updatedEvent = {
26 |       ...event.data,
27 |       summary: updates.summary || event.data.summary,
28 |       description: updates.description || event.data.description,
29 |       start: updates.startTime
30 |         ? {
31 |             dateTime: convertToTimeZone(updates.startTime, process.env.GOOGLE_TIME_ZONE || "Asia/Taipei"),
32 |             timeZone: process.env.GOOGLE_TIME_ZONE || "Asia/Taipei",
33 |           }
34 |         : event.data.start,
35 |       end: updates.endTime
36 |         ? {
37 |             dateTime: convertToTimeZone(updates.endTime, process.env.GOOGLE_TIME_ZONE || "Asia/Taipei"),
38 |             timeZone: process.env.GOOGLE_TIME_ZONE || "Asia/Taipei",
39 |           }
40 |         : event.data.end,
41 |       attendees: updates.attendees ? updates.attendees.map((email) => ({ email })) : event.data.attendees,
42 |     };
43 | 
44 |     const result = await calendar.events.update({
45 |       calendarId: process.env.GOOGLE_CALENDAR_ID || "primary",
46 |       eventId: eventId,
47 |       requestBody: updatedEvent,
48 |       sendUpdates: "all",
49 |     });
50 | 
51 |     return {
52 |       success: true,
53 |       data: result.data,
54 |     };
55 |   } catch (error) {
56 |     return {
57 |       success: false,
58 |       error: error instanceof Error ? error.message : "Error updating event",
59 |     };
60 |   }
61 | }
62 | 
```

--------------------------------------------------------------------------------
/src/calendar-tools/calendarTools.ts:
--------------------------------------------------------------------------------

```typescript
  1 | export const CREATE_EVENT_TOOL = {
  2 |   name: "create_event",
  3 |   description: "Create a new Google Calendar event",
  4 |   inputSchema: {
  5 |     type: "object",
  6 |     properties: {
  7 |       summary: {
  8 |         type: "string",
  9 |         description: "Event title",
 10 |       },
 11 |       description: {
 12 |         type: "string",
 13 |         description: "Event description",
 14 |       },
 15 |       startTime: {
 16 |         type: "string",
 17 |         description: "Event start time (ISO format)",
 18 |       },
 19 |       endTime: {
 20 |         type: "string",
 21 |         description: "Event end time (ISO format)",
 22 |       },
 23 |       attendees: {
 24 |         type: "array",
 25 |         items: {
 26 |           type: "string",
 27 |         },
 28 |         description: "List of attendee email addresses",
 29 |       },
 30 |     },
 31 |     required: ["summary", "startTime", "endTime"],
 32 |   },
 33 | };
 34 | 
 35 | export const LIST_EVENTS_TOOL = {
 36 |   name: "list_events",
 37 |   description: "List Google Calendar events",
 38 |   inputSchema: {
 39 |     type: "object",
 40 |     properties: {
 41 |       timeMin: {
 42 |         type: "string",
 43 |         description: "Start time (ISO format)",
 44 |       },
 45 |       maxResults: {
 46 |         type: "number",
 47 |         description: "Maximum number of results",
 48 |       },
 49 |     },
 50 |   },
 51 | };
 52 | 
 53 | export const UPDATE_EVENT_TOOL = {
 54 |   name: "update_event",
 55 |   description: "Update an existing Google Calendar event",
 56 |   inputSchema: {
 57 |     type: "object",
 58 |     properties: {
 59 |       eventId: {
 60 |         type: "string",
 61 |         description: "ID of the event to update",
 62 |       },
 63 |       updates: {
 64 |         type: "object",
 65 |         properties: {
 66 |           summary: {
 67 |             type: "string",
 68 |             description: "New event title",
 69 |           },
 70 |           description: {
 71 |             type: "string",
 72 |             description: "New event description",
 73 |           },
 74 |           startTime: {
 75 |             type: "string",
 76 |             description: "New start time",
 77 |           },
 78 |           endTime: {
 79 |             type: "string",
 80 |             description: "New end time",
 81 |           },
 82 |           attendees: {
 83 |             type: "array",
 84 |             items: {
 85 |               type: "string",
 86 |             },
 87 |             description: "New list of attendees",
 88 |           },
 89 |         },
 90 |       },
 91 |     },
 92 |     required: ["eventId", "updates"],
 93 |   },
 94 | };
 95 | 
 96 | export const DELETE_EVENT_TOOL = {
 97 |   name: "delete_event",
 98 |   description: "Delete a Google Calendar event",
 99 |   inputSchema: {
100 |     type: "object",
101 |     properties: {
102 |       eventId: {
103 |         type: "string",
104 |         description: "ID of the event to delete",
105 |       },
106 |     },
107 |     required: ["eventId"],
108 |   },
109 | };
110 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
  6 | import { calendar_v3 } from "googleapis";
  7 | 
  8 | import { createEvent, deleteEvent, listEvents, tools, updateEvent } from "./calendar-tools/_index.js";
  9 | import { getAuthClient } from "./utils/auth.js";
 10 | 
 11 | const server = new Server(
 12 |   {
 13 |     name: "mcp-server/calendar_executor",
 14 |     version: "0.0.1",
 15 |   },
 16 |   {
 17 |     capabilities: {
 18 |       description: "An MCP server providing Google Calendar integration!",
 19 |       tools: {},
 20 |     },
 21 |   }
 22 | );
 23 | 
 24 | server.setRequestHandler(ListToolsRequestSchema, async () => ({
 25 |   tools,
 26 | }));
 27 | 
 28 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
 29 |   try {
 30 |     const { name, arguments: args } = request.params;
 31 | 
 32 |     if (!args) {
 33 |       throw new Error("No parameters provided");
 34 |     }
 35 | 
 36 |     if (name === "create_event") {
 37 |       const { summary, description, startTime, endTime, attendees } = args as {
 38 |         summary: string;
 39 |         description: string;
 40 |         startTime: string;
 41 |         endTime: string;
 42 |         attendees?: string[];
 43 |       };
 44 | 
 45 |       const result = await createEvent({
 46 |         summary,
 47 |         description,
 48 |         startTime,
 49 |         endTime,
 50 |         attendees,
 51 |       });
 52 | 
 53 |       if (!result.success) {
 54 |         return {
 55 |           content: [{ type: "text", text: `Error: ${result.error}` }],
 56 |           isError: true,
 57 |         };
 58 |       }
 59 | 
 60 |       return {
 61 |         content: [{ type: "text", text: `Successfully created event: ${result.data?.summary || ""}` }],
 62 |         isError: false,
 63 |       };
 64 |     }
 65 | 
 66 |     if (name === "list_events") {
 67 |       const { timeMin, maxResults } = args as {
 68 |         timeMin?: string;
 69 |         maxResults?: number;
 70 |       };
 71 | 
 72 |       const result = await listEvents({
 73 |         timeMin: timeMin || new Date().toISOString(),
 74 |         maxResults: maxResults || 10,
 75 |       });
 76 | 
 77 |       if (!result.success) {
 78 |         return {
 79 |           content: [{ type: "text", text: `Error: ${result.error}` }],
 80 |           isError: true,
 81 |         };
 82 |       }
 83 | 
 84 |       return {
 85 |         content: [{ type: "text", text: formatEventsList(result.data || []) }],
 86 |         isError: false,
 87 |       };
 88 |     }
 89 | 
 90 |     if (name === "update_event") {
 91 |       const { eventId, updates } = args as {
 92 |         eventId: string;
 93 |         updates: {
 94 |           summary?: string;
 95 |           description?: string;
 96 |           startTime?: string;
 97 |           endTime?: string;
 98 |           attendees?: string[];
 99 |         };
100 |       };
101 | 
102 |       const result = await updateEvent(eventId, updates);
103 | 
104 |       if (!result.success) {
105 |         return {
106 |           content: [{ type: "text", text: `Error: ${result.error}` }],
107 |           isError: true,
108 |         };
109 |       }
110 | 
111 |       return {
112 |         content: [{ type: "text", text: `Successfully updated event: ${result.data?.summary || ""}` }],
113 |         isError: false,
114 |       };
115 |     }
116 | 
117 |     if (name === "delete_event") {
118 |       const { eventId } = args as {
119 |         eventId: string;
120 |       };
121 | 
122 |       const result = await deleteEvent(eventId);
123 | 
124 |       if (!result.success) {
125 |         return {
126 |           content: [{ type: "text", text: `Error: ${result.error}` }],
127 |           isError: true,
128 |         };
129 |       }
130 | 
131 |       return {
132 |         content: [{ type: "text", text: "Successfully deleted event" }],
133 |         isError: false,
134 |       };
135 |     }
136 | 
137 |     return {
138 |       content: [{ type: "text", text: `Error: ${name} is an unknown tool` }],
139 |       isError: true,
140 |     };
141 |   } catch (error) {
142 |     return {
143 |       content: [
144 |         {
145 |           type: "text",
146 |           text: `Error: ${error instanceof Error ? error.message : String(error)}`,
147 |         },
148 |       ],
149 |       isError: true,
150 |     };
151 |   }
152 | });
153 | 
154 | async function runServer() {
155 |   try {
156 |     // 先初始化認證
157 |     await getAuthClient();
158 | 
159 |     const transport = new StdioServerTransport();
160 |     await server.connect(transport);
161 |     console.log("MCP Calendar Server started");
162 |   } catch (error) {
163 |     console.error("Server startup failed:", error);
164 |     process.exit(1);
165 |   }
166 | }
167 | 
168 | runServer().catch((error) => {
169 |   console.error("Server encountered a critical error:", error);
170 |   process.exit(1);
171 | });
172 | 
173 | function formatEventsList(events: calendar_v3.Schema$Event[]): string {
174 |   return events
175 |     .map((event) => {
176 |       return `
177 | ID: ${event.id}
178 | Event: ${event.summary}
179 | Description: ${event.description || "None"}
180 | Start Time: ${new Date(event.start?.dateTime || "").toLocaleString()}
181 | End Time: ${new Date(event.end?.dateTime || "").toLocaleString()}
182 | Attendees: ${event.attendees?.map((a: calendar_v3.Schema$EventAttendee) => a.email).join(", ") || "None"}
183 |     `.trim();
184 |     })
185 |     .join("\n\n");
186 | }
187 | 
```