# 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 |
```