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

```
├── .gitignore
├── devbox.json
├── devbox.lock
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── index.ts
│   └── types.ts
└── tsconfig.json
```

# Files

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

```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
5 | .client_secret.json
6 | .gcal-tokens-*.json
7 | .gcal-settings.json
8 | .devbox
9 | 
```

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

```markdown
  1 | # Google Calendar MCP Server
  2 | 
  3 | Interact with your Google Calendar through Claude Desktop using the Model Context Protocol (MCP).
  4 | 
  5 | This is a TypeScript-based MCP server that implements Google Calendar integration. It demonstrates core MCP concepts while providing:
  6 | 
  7 | - Calendar event management through MCP URIs
  8 | - Tools for creating and modifying events
  9 | - Prompts for generating calendar insights
 10 | 
 11 | ## Features
 12 | 
 13 | ### Resources
 14 | - Access calendar events via MCP URIs
 15 | - Each event has title, time, description, and attendees
 16 | - Structured event data with proper mime types
 17 | 
 18 | ### Tools
 19 | - `create_event` - Create new calendar events
 20 |   - Takes title, time, and other event details as parameters
 21 |   - Directly interfaces with Google Calendar API
 22 | - `list_events` - View upcoming calendar events
 23 | - [Add other tools you've implemented]
 24 | 
 25 | ### Prompts
 26 | - `analyze_schedule` - Generate insights about your calendar
 27 |   - Includes upcoming events as embedded resources
 28 |   - Returns structured prompt for LLM analysis
 29 | - [Add other prompts you've implemented]
 30 | 
 31 | ## Prerequisites
 32 | 
 33 | - Node.js (v14 or higher)
 34 | - A Google Cloud Project with Calendar API enabled
 35 | - OAuth 2.0 Client credentials
 36 | 
 37 | ## Development
 38 | 
 39 | Install devbox by following instructions at [devbox.sh](https://www.jetpack.io/devbox)
 40 | ```bash
 41 | curl -fsSL https://get.jetpack.io/devbox | bash
 42 | ```
 43 | 
 44 | Initialize devbox in the project directory:
 45 | ```bash
 46 | devbox init
 47 | ```
 48 | 
 49 | Start the devbox shell:
 50 | ```bash
 51 | devbox shell
 52 | ```
 53 | 
 54 | Install dependencies:
 55 | ```bash
 56 | npm install
 57 | ```
 58 | 
 59 | Build the server:
 60 | ```bash
 61 | npm run build
 62 | ```
 63 | 
 64 | For development with auto-rebuild:
 65 | ```bash
 66 | npm run watch
 67 | ```
 68 | 
 69 | ## Installation
 70 | 
 71 | To use with Claude Desktop, add the server config:
 72 | 
 73 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
 74 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
 75 | 
 76 | ```json
 77 | {
 78 |   "mcpServers": {
 79 |     "Google Calendar": {
 80 |       "command": "/path/to/Google Calendar/build/index.js"
 81 |     }
 82 |   }
 83 | }
 84 | ```
 85 | 
 86 | ## First-Time Setup
 87 | 
 88 | 1. Set up Google Cloud credentials:
 89 |    - Go to [Google Cloud Console](https://console.cloud.google.com)
 90 |    - Create a new project or select an existing one
 91 |    - Enable the Google Calendar API
 92 |    - Create OAuth 2.0 credentials (Desktop application type)
 93 |    - Download the client secret JSON file
 94 |    - Rename it to `.client_secret.json` and place it in the project root
 95 | 
 96 | 2. Initial Authentication:
 97 |    - When first running the server, it will provide an authentication URL
 98 |    - Visit the URL in your browser
 99 |    - Grant the requested permissions
100 |    - Copy the provided authorization code
101 |    - Paste the code back into the CLI prompt
102 | 
103 | ### Debugging
104 | 
105 | Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script:
106 | 
107 | ```bash
108 | npm run inspector
109 | ```
110 | 
111 | The Inspector will provide a URL to access debugging tools in your browser.
112 | 
```

--------------------------------------------------------------------------------
/devbox.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "$schema":  "https://raw.githubusercontent.com/jetify-com/devbox/0.13.6/.schema/devbox.schema.json",
 3 |   "packages": ["nodejs@latest"],
 4 |   "shell": {
 5 |     "init_hook": [
 6 |       "echo 'Welcome to devbox!' > /dev/null"
 7 |     ],
 8 |     "scripts": {
 9 |       "test": [
10 |         "echo \"Error: no test specified\" && exit 1"
11 |       ]
12 |     }
13 |   }
14 | }
15 | 
```

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

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

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

```json
 1 | {
 2 |   "name": "gcal-mcp-server",
 3 |   "version": "0.1.0",
 4 |   "description": "Interact with your google calendar",
 5 |   "private": true,
 6 |   "type": "module",
 7 |   "bin": {
 8 |     "Google Calendar": "./build/index.js"
 9 |   },
10 |   "files": [
11 |     "build"
12 |   ],
13 |   "scripts": {
14 |     "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
15 |     "prepare": "npm run build",
16 |     "watch": "tsc --watch",
17 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js",
18 |     "auth": "npm run build && node build/index.js auth"
19 |   },
20 |   "dependencies": {
21 |     "@modelcontextprotocol/sdk": "^1.6.1",
22 |     "google-auth-library": "^9.15.0",
23 |     "googleapis": "^144.0.0",
24 |     "zod": "^3.23.8",
25 |     "zod-to-json-schema": "^3.23.5"
26 |   },
27 |   "devDependencies": {
28 |     "@types/node": "^20.11.24",
29 |     "typescript": "^5.3.3"
30 |   }
31 | }
32 | 
```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // ... existing imports ...
 2 | import { z } from "zod";
 3 | 
 4 | // Add schemas at the top of the file after imports
 5 | export const CreateEventArgsSchema = z.object({
 6 |   accountId: z.string(),
 7 |   calendarId: z.string().optional(),
 8 |   summary: z.string(),
 9 |   description: z.string().optional(),
10 |   location: z.string().optional(),
11 |   start: z.string(),
12 |   end: z.string()
13 | });
14 | 
15 | export const SearchEventsArgsSchema = z.object({
16 |   accountId: z.string(),
17 |   query: z.string()
18 | });
19 | 
20 | export const ListEventsArgsSchema = z.object({
21 |   accountId: z.string(),
22 |   calendarId: z.string().optional(),
23 |   maxResults: z.number().optional().default(10),
24 |   timeMin: z.string().optional(),
25 |   timeMax: z.string().optional()
26 | });
27 | 
28 | export const SetCalendarDefaultsArgsSchema = z.object({
29 |   accountId: z.string(),
30 |   calendarId: z.string().optional()
31 | });
32 | 
33 | export const ListCalendarsArgsSchema = z.object({
34 |   accountId: z.string()
35 | });
36 | 
37 | export type CreateEventArgs = z.infer<typeof CreateEventArgsSchema>;
38 | export type SearchEventsArgs = z.infer<typeof SearchEventsArgsSchema>;
39 | export type ListEventsArgs = z.infer<typeof ListEventsArgsSchema>;
40 | export type SetCalendarDefaultsArgs = z.infer<typeof SetCalendarDefaultsArgsSchema>;
41 | export type ListCalendarsArgs = z.infer<typeof ListCalendarsArgsSchema>;
42 | 
43 | // ... existing code ...
44 | 
45 | // Then in the CallToolRequestSchema handler, update the cases:
46 | 
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import {
  6 |   CallToolRequestSchema,
  7 |   ListResourcesRequestSchema,
  8 |   ListToolsRequestSchema,
  9 |   ReadResourceRequestSchema,
 10 | } from "@modelcontextprotocol/sdk/types.js";
 11 | import fs from "fs";
 12 | import { google } from "googleapis";
 13 | import path from "path";
 14 | import { OAuth2Client } from 'google-auth-library';
 15 | import { CreateEventArgsSchema,
 16 |   SearchEventsArgsSchema,
 17 |   ListEventsArgsSchema,
 18 |   SetCalendarDefaultsArgsSchema,
 19 |   ListCalendarsArgsSchema,
 20 | } from "./types.js";
 21 | import { zodToJsonSchema } from "zod-to-json-schema";
 22 | import { z } from "zod";
 23 | 
 24 | const calendar = google.calendar("v3");
 25 | 
 26 | // Store multiple auth instances
 27 | const authInstances: { [accountId: string]: any } = {};
 28 | 
 29 | interface Settings {
 30 |   defaultAccountId?: string;
 31 |   defaultCalendarId?: string;
 32 | }
 33 | 
 34 | function getSettingsPath() {
 35 |   return path.join(
 36 |     path.dirname(new URL(import.meta.url).pathname),
 37 |     "..",
 38 |     ".gcal-settings.json"
 39 |   );
 40 | }
 41 | 
 42 | function loadSettings(): Settings {
 43 |   try {
 44 |     return JSON.parse(fs.readFileSync(getSettingsPath(), "utf-8"));
 45 |   } catch {
 46 |     return {};
 47 |   }
 48 | }
 49 | 
 50 | function saveSettings(settings: Settings) {
 51 |   fs.writeFileSync(getSettingsPath(), JSON.stringify(settings, null, 2));
 52 | }
 53 | 
 54 | function getAuthInstance(accountId?: string): OAuth2Client {
 55 |   if (!accountId) {
 56 |     const settings = loadSettings();
 57 |     accountId = settings.defaultAccountId;
 58 | 
 59 |     if (!accountId) {
 60 |       const firstAccount = Object.keys(authInstances)[0];
 61 |       if (!firstAccount) throw new Error("No authenticated accounts found");
 62 |       return authInstances[firstAccount];
 63 |     }
 64 |   }
 65 |   const auth = authInstances[accountId];
 66 |   if (!auth) throw new Error(`Account ${accountId} not found`);
 67 |   return auth;
 68 | }
 69 | 
 70 | interface ToolCapability {
 71 |   description: string;
 72 |   inputSchema: any;
 73 |   outputSchema: any;
 74 | }
 75 | 
 76 | interface ServerCapabilities {
 77 |   resources: {
 78 |     [mimeType: string]: {
 79 |       description: string;
 80 |     };
 81 |   };
 82 |   tools: {
 83 |     [name: string]: ToolCapability;
 84 |   };
 85 | }
 86 | 
 87 | // Store tool definitions for reuse
 88 | const toolDefinitions = {
 89 |   create_event: {
 90 |     description: "Create a new calendar event in Google Calendar. Provide the account ID, event summary, start and end times, and optionally a calendar ID, location, and description.",
 91 |     inputSchema: zodToJsonSchema(CreateEventArgsSchema),
 92 |     outputSchema: {
 93 |       type: "object",
 94 |       properties: {
 95 |         content: {
 96 |           type: "array",
 97 |           items: {
 98 |             type: "object",
 99 |             properties: {
100 |               type: { type: "string", enum: ["text"] },
101 |               text: { type: "string" }
102 |             },
103 |             required: ["type", "text"]
104 |           }
105 |         }
106 |       },
107 |       required: ["content"]
108 |     }
109 |   },
110 |   search_events: {
111 |     description: "Search for calendar events in Google Calendar using a text query. Returns events matching the query text in their title, description, or location.",
112 |     inputSchema: zodToJsonSchema(SearchEventsArgsSchema),
113 |     outputSchema: {
114 |       type: "object",
115 |       properties: {
116 |         content: {
117 |           type: "array",
118 |           items: {
119 |             type: "object",
120 |             properties: {
121 |               type: { type: "string", enum: ["text"] },
122 |               text: { type: "string" }
123 |             },
124 |             required: ["type", "text"]
125 |           }
126 |         }
127 |       },
128 |       required: ["content"]
129 |     }
130 |   },
131 |   list_events: {
132 |     description: "List upcoming calendar events from Google Calendar. Specify the account ID, optional calendar ID, maximum number of results to return, and optional time range filters.",
133 |     inputSchema: zodToJsonSchema(ListEventsArgsSchema),
134 |     outputSchema: {
135 |       type: "object",
136 |       properties: {
137 |         content: {
138 |           type: "array",
139 |           items: {
140 |             type: "object",
141 |             properties: {
142 |               type: { type: "string", enum: ["text"] },
143 |               text: { type: "string" }
144 |             },
145 |             required: ["type", "text"]
146 |           }
147 |         }
148 |       },
149 |       required: ["content"]
150 |     }
151 |   },
152 |   set_calendar_defaults: {
153 |     description: "Set the default Google account and optionally the default calendar to use for calendar operations. These defaults will be used when account or calendar are not explicitly specified.",
154 |     inputSchema: zodToJsonSchema(SetCalendarDefaultsArgsSchema),
155 |     outputSchema: {
156 |       type: "object",
157 |       properties: {
158 |         content: {
159 |           type: "array",
160 |           items: {
161 |             type: "object",
162 |             properties: {
163 |               type: { type: "string", enum: ["text"] },
164 |               text: { type: "string" }
165 |             },
166 |             required: ["type", "text"]
167 |           }
168 |         }
169 |       },
170 |       required: ["content"]
171 |     }
172 |   },
173 |   list_calendar_accounts: {
174 |     description: "List all authenticated Google Calendar accounts that the user has connected to this server. Shows which account is set as default.",
175 |     inputSchema: zodToJsonSchema(z.object({})),
176 |     outputSchema: {
177 |       type: "object",
178 |       properties: {
179 |         content: {
180 |           type: "array",
181 |           items: {
182 |             type: "object",
183 |             properties: {
184 |               type: { type: "string", enum: ["text"] },
185 |               text: { type: "string" }
186 |             },
187 |             required: ["type", "text"]
188 |           }
189 |         }
190 |       },
191 |       required: ["content"]
192 |     }
193 |   },
194 |   list_calendars: {
195 |     description: "List all available calendars for a specific Google account. Requires an account ID and returns calendar names, IDs, and other metadata.",
196 |     inputSchema: zodToJsonSchema(ListCalendarsArgsSchema),
197 |     outputSchema: {
198 |       type: "object",
199 |       properties: {
200 |         content: {
201 |           type: "array",
202 |           items: {
203 |             type: "object",
204 |             properties: {
205 |               type: { type: "string", enum: ["text"] },
206 |               text: { type: "string" }
207 |             },
208 |             required: ["type", "text"]
209 |           }
210 |         }
211 |       },
212 |       required: ["content"]
213 |     }
214 |   }
215 | };
216 | 
217 | // Initialize the MCP server with Google Calendar capabilities
218 | // This server allows models to access and manage Google Calendar events and settings
219 | const server = new McpServer({
220 |   name: "Google Calendar",
221 |   version: "0.1.0",
222 |   protocolVersion: "2.0"
223 | }, {
224 |   capabilities: {
225 |     tools: toolDefinitions
226 |   }
227 | });
228 | 
229 | // Register tools
230 | server.tool(
231 |   "create_event",
232 |   "Create a new calendar event in Google Calendar. Provide the account ID, event summary, start and end times, and optionally a calendar ID, location, and description.",
233 |   CreateEventArgsSchema.shape,
234 |   async ({ summary, description, start, end, calendarId, location, accountId }: z.infer<typeof CreateEventArgsSchema>) => {
235 |     // Create a new event in Google Calendar with the provided details
236 |     // Returns the created event ID and a confirmation message
237 |     const auth = getAuthInstance(accountId);
238 |     google.options({ auth });
239 |     const settings = loadSettings();
240 | 
241 |     try {
242 |       const event = await calendar.events.insert({
243 |         calendarId: calendarId || settings.defaultCalendarId || 'primary',
244 |         requestBody: {
245 |           summary,
246 |           description,
247 |           start: { dateTime: start },
248 |           end: { dateTime: end },
249 |           location
250 |         }
251 |       });
252 | 
253 |       return {
254 |         content: [{
255 |           type: "text",
256 |           text: `Created event: ${event.data.htmlLink}`
257 |         }]
258 |       };
259 |     } catch (error: any) {
260 |       throw new Error(`Failed to create event: ${error.message}`);
261 |     }
262 |   }
263 | );
264 | 
265 | server.tool(
266 |   "search_events",
267 |   "Search for calendar events in Google Calendar using a text query. Returns events matching the query text in their title, description, or location.",
268 |   SearchEventsArgsSchema.shape,
269 |   async ({ query, accountId }: z.infer<typeof SearchEventsArgsSchema>) => {
270 |     // Search for events in Google Calendar matching the query string
271 |     // Returns a list of matching events with their details
272 |     const auth = getAuthInstance(accountId);
273 |     google.options({ auth });
274 | 
275 |     try {
276 |       const res = await calendar.events.list({
277 |         calendarId: 'primary',
278 |         q: query,
279 |         maxResults: 10
280 |       });
281 | 
282 |       const eventList = res.data.items
283 |         ?.map(event => `${event.summary} (${event.start?.dateTime || event.start?.date})`)
284 |         .join("\n");
285 | 
286 |       return {
287 |         content: [{
288 |           type: "text",
289 |           text: `Found ${res.data.items?.length ?? 0} events:\n${eventList}`
290 |         }]
291 |       };
292 |     } catch (error: any) {
293 |       throw new Error(`Failed to search events: ${error.message}`);
294 |     }
295 |   }
296 | );
297 | 
298 | server.tool(
299 |   "list_events",
300 |   "List upcoming calendar events from Google Calendar. Specify the account ID, optional calendar ID, maximum number of results to return, and optional time range filters.",
301 |   ListEventsArgsSchema.shape,
302 |   async ({ accountId, calendarId, maxResults = 10, timeMin, timeMax }: z.infer<typeof ListEventsArgsSchema>) => {
303 |     // List upcoming events from the specified calendar
304 |     // Returns events with their titles, times, and other details
305 |     const auth = getAuthInstance(accountId);
306 |     google.options({ auth });
307 | 
308 |     const defaultTimeMin = new Date();
309 |     defaultTimeMin.setDate(defaultTimeMin.getDate() - 7);
310 | 
311 |     try {
312 |       if (calendarId) {
313 |         try {
314 |           await calendar.calendars.get({ calendarId });
315 |         } catch (error: any) {
316 |           throw new Error(`Calendar ${calendarId} not found: ${error.message}`);
317 |         }
318 | 
319 |         const params = {
320 |           calendarId,
321 |           timeMin: timeMin || defaultTimeMin.toISOString(),
322 |           timeMax: timeMax || undefined,
323 |           maxResults,
324 |           singleEvents: true,
325 |           orderBy: 'startTime'
326 |         };
327 | 
328 |         const res = await calendar.events.list(params);
329 |         const eventList = res.data.items
330 |           ?.map(event => `- ${event.summary || 'Untitled'} (${event.start?.dateTime || event.start?.date})`)
331 |           .join("\n");
332 | 
333 |         return {
334 |           content: [{
335 |             type: "text",
336 |             text: `Events for calendar ${calendarId} in account ${accountId}:\n${eventList}`
337 |           }]
338 |         };
339 |       }
340 | 
341 |       const calendarsResponse = await calendar.calendarList.list();
342 |       const calendars = calendarsResponse.data.items || [];
343 |       let allEvents = [];
344 | 
345 |       for (const cal of calendars) {
346 |         if (!cal.id) continue;
347 | 
348 |         const params = {
349 |           calendarId: cal.id,
350 |           timeMin: timeMin || defaultTimeMin.toISOString(),
351 |           timeMax: timeMax || undefined,
352 |           maxResults,
353 |           singleEvents: true,
354 |           orderBy: 'startTime'
355 |         };
356 | 
357 |         const res = await calendar.events.list(params);
358 |         const events = res.data.items || [];
359 |         allEvents.push(...events.map(event => ({
360 |           summary: event.summary || 'Untitled',
361 |           calendar: cal.summary,
362 |           start: event.start?.dateTime || event.start?.date
363 |         })));
364 |       }
365 | 
366 |       allEvents.sort((a, b) => {
367 |         const dateA = a.start ? new Date(a.start).getTime() : 0;
368 |         const dateB = b.start ? new Date(b.start).getTime() : 0;
369 |         return dateA - dateB;
370 |       });
371 | 
372 |       allEvents = allEvents.slice(0, maxResults);
373 | 
374 |       const eventList = allEvents
375 |         .map(event => `- [${event.calendar}] ${event.summary} (${event.start})`)
376 |         .join("\n");
377 | 
378 |       return {
379 |         content: [{
380 |           type: "text",
381 |           text: `Events across all calendars for account ${accountId}:\n${eventList}`
382 |         }]
383 |       };
384 |     } catch (error: any) {
385 |       throw new Error(`Failed to list events: ${error.message}`);
386 |     }
387 |   }
388 | );
389 | 
390 | server.tool(
391 |   "set_calendar_defaults",
392 |   "Set the default Google account and optionally the default calendar to use for calendar operations. These defaults will be used when account or calendar are not explicitly specified.",
393 |   SetCalendarDefaultsArgsSchema.shape,
394 |   async ({ accountId, calendarId }: z.infer<typeof SetCalendarDefaultsArgsSchema>) => {
395 |     // Set the default Google account and calendar to use for operations
396 |     // These defaults will be used when parameters are not explicitly provided
397 |     if (!authInstances[accountId]) {
398 |       throw new Error(`Account ${accountId} not found`);
399 |     }
400 | 
401 |     const auth = getAuthInstance(accountId);
402 |     google.options({ auth });
403 | 
404 |     try {
405 |       if (calendarId) {
406 |         try {
407 |           await calendar.calendars.get({ calendarId });
408 |         } catch (error: any) {
409 |           throw new Error(`Calendar ${calendarId} not found: ${error.message}`);
410 |         }
411 |       }
412 | 
413 |       const settings = loadSettings();
414 |       settings.defaultAccountId = accountId;
415 |       if (calendarId) settings.defaultCalendarId = calendarId;
416 |       saveSettings(settings);
417 | 
418 |       return {
419 |         content: [{
420 |           type: "text",
421 |           text: `Default account set to ${accountId}${calendarId ? ` and calendar set to ${calendarId}` : ''}`
422 |         }]
423 |       };
424 |     } catch (error: any) {
425 |       throw new Error(`Failed to set calendar defaults: ${error.message}`);
426 |     }
427 |   }
428 | );
429 | 
430 | server.tool(
431 |   "list_calendar_accounts",
432 |   "List all authenticated Google Calendar accounts that the user has connected to this server. Shows which account is set as default.",
433 |   z.object({}).shape,
434 |   async () => {
435 |     // List all authenticated Google Calendar accounts
436 |     // Indicates which account is currently set as the default
437 |     try {
438 |       const accounts = Object.keys(authInstances).map(accountId => {
439 |         const isDefault = loadSettings().defaultAccountId === accountId;
440 |         return `${isDefault ? '* ' : '- '}${accountId}`;
441 |       }).join('\n');
442 | 
443 |       return {
444 |         content: [{
445 |           type: "text",
446 |           text: accounts ? `Available accounts:\n${accounts}\n(* indicates default account)` : "No accounts configured"
447 |         }]
448 |       };
449 |     } catch (error: any) {
450 |       throw new Error(`Failed to list calendar accounts: ${error.message}`);
451 |     }
452 |   }
453 | );
454 | 
455 | server.tool(
456 |   "list_calendars",
457 |   "List all available calendars for a specific Google account. Requires an account ID and returns calendar names, IDs, and other metadata.",
458 |   ListCalendarsArgsSchema.shape,
459 |   async ({ accountId }: z.infer<typeof ListCalendarsArgsSchema>) => {
460 |     // List all calendars available in the specified Google account
461 |     // Returns calendar IDs, names, and attributes
462 |     const auth = getAuthInstance(accountId);
463 |     google.options({ auth });
464 | 
465 |     try {
466 |       const calendars = await calendar.calendarList.list();
467 |       const defaultCalendarId = loadSettings().defaultCalendarId;
468 | 
469 |       const calendarList = calendars.data.items?.map(cal => {
470 |         const isDefault = cal.id === defaultCalendarId;
471 |         return `${isDefault ? '* ' : '- '}${cal.summary} (${cal.id})`;
472 |       }).join('\n');
473 | 
474 |       return {
475 |         content: [{
476 |           type: "text",
477 |           text: calendarList ? `Available calendars:\n${calendarList}\n(* indicates default calendar)` : "No calendars found"
478 |         }]
479 |       };
480 |     } catch (error: any) {
481 |       throw new Error(`Failed to list calendars: ${error.message}`);
482 |     }
483 |   }
484 | );
485 | 
486 | // Helper to get tokens path for an account
487 | const getTokensPath = (accountId: string) => {
488 |   return path.join(
489 |     path.dirname(new URL(import.meta.url).pathname),
490 |     "..",
491 |     `.gcal-tokens-${accountId}.json`
492 |   );
493 | };
494 | 
495 | // Create OAuth client from credentials file
496 | function getOAuthClient() {
497 |   const credentials = JSON.parse(
498 |     fs.readFileSync(
499 |       path.join(path.dirname(new URL(import.meta.url).pathname), "..", ".client_secret.json"),
500 |       "utf-8"
501 |     )
502 |   );
503 | 
504 |   return new google.auth.OAuth2(
505 |     credentials.installed.client_id,
506 |     credentials.installed.client_secret,
507 |     "urn:ietf:wg:oauth:2.0:oob"
508 |   );
509 | }
510 | 
511 | async function loadCredentialsAndRunServer() {
512 |   const tokenFiles = fs.readdirSync(path.join(path.dirname(new URL(import.meta.url).pathname), ".."))
513 |     .filter(f => f.startsWith('.gcal-tokens-'));
514 | 
515 |   if (tokenFiles.length === 0) {
516 |     throw new Error("No tokens found. Please run with 'auth <account-id>' first.");
517 |   }
518 | 
519 |   // Initialize auth for each account
520 |   for (const file of tokenFiles) {
521 |     const accountId = file.replace('.gcal-tokens-', '').replace('.json', '');
522 |     const tokens = JSON.parse(
523 |       fs.readFileSync(getTokensPath(accountId), "utf-8")
524 |     );
525 | 
526 |     const oAuth2Client = getOAuthClient();
527 |     oAuth2Client.setCredentials(tokens);
528 | 
529 |     // Set up token refresh handler
530 |     oAuth2Client.on('tokens', (tokens) => {
531 |       const allTokens = {
532 |         ...JSON.parse(fs.readFileSync(getTokensPath(accountId), "utf-8")),
533 |         ...tokens
534 |       };
535 |       fs.writeFileSync(getTokensPath(accountId), JSON.stringify(allTokens));
536 |     });
537 | 
538 |     authInstances[accountId] = oAuth2Client;
539 |   }
540 | 
541 |   const transport = new StdioServerTransport();
542 |   await server.connect(transport);
543 | }
544 | 
545 | // Helper function to get authorization code from user
546 | async function getAuthorizationCode(): Promise<string> {
547 |   const readline = await import('readline');
548 |   const rl = readline.createInterface({
549 |     input: process.stdin,
550 |     output: process.stdout
551 |   });
552 | 
553 |   return new Promise((resolve) => {
554 |     rl.question('Enter the authorization code: ', (code: string) => {
555 |       rl.close();
556 |       resolve(code.trim());
557 |     });
558 |   });
559 | }
560 | 
561 | async function authenticateAccount(accountId: string) {
562 |   const oAuth2Client = getOAuthClient();
563 | 
564 |   const authUrl = oAuth2Client.generateAuthUrl({
565 |     access_type: 'offline',
566 |     scope: ['https://www.googleapis.com/auth/calendar'],
567 |     prompt: 'consent'
568 |   });
569 | 
570 |   console.log(`Please authorize this app by visiting: ${authUrl}`);
571 | 
572 |   const code = await getAuthorizationCode();
573 |   const { tokens } = await oAuth2Client.getToken(code);
574 |   oAuth2Client.setCredentials(tokens);
575 | 
576 |   fs.writeFileSync(
577 |     getTokensPath(accountId),
578 |     JSON.stringify(tokens)
579 |   );
580 | 
581 |   console.log(`Successfully authenticated ${accountId}`);
582 |   return oAuth2Client;
583 | }
584 | 
585 | if (process.argv[2] === "auth") {
586 |   const accountId = process.argv[3];
587 |   if (!accountId) {
588 |     throw new Error("Please provide an account ID");
589 |   }
590 |   authenticateAccount(accountId).catch(error => {
591 |     console.error("Authentication failed:", error.message);
592 |     process.exit(1);
593 |   });
594 | } else {
595 |   loadCredentialsAndRunServer().catch(error => {
596 |     console.error("Server failed:", error.message);
597 |     process.exit(1);
598 |   });
599 | }
600 | 
```