@node2flow/google-calendar-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Google Calendar MCP Server
4
+ *
5
+ * Community edition — connects directly to Google Calendar API v3.
6
+ *
7
+ * Usage (stdio - for Claude Desktop / Cursor / VS Code):
8
+ * GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=xxx GOOGLE_REFRESH_TOKEN=xxx npx @node2flow/google-calendar-mcp
9
+ *
10
+ * Usage (HTTP - Streamable HTTP transport):
11
+ * GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=xxx GOOGLE_REFRESH_TOKEN=xxx npx @node2flow/google-calendar-mcp --http
12
+ */
13
+
14
+ import { randomUUID } from 'node:crypto';
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import {
17
+ StreamableHTTPServerTransport,
18
+ } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
19
+ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
20
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
21
+
22
+ import { createServer } from './server.js';
23
+ import { TOOLS } from './tools.js';
24
+
25
+ function getConfig() {
26
+ const clientId = process.env.GOOGLE_CLIENT_ID;
27
+ const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
28
+ const refreshToken = process.env.GOOGLE_REFRESH_TOKEN;
29
+ if (!clientId || !clientSecret || !refreshToken) return null;
30
+ return { clientId, clientSecret, refreshToken };
31
+ }
32
+
33
+ async function startStdio() {
34
+ const config = getConfig();
35
+ const server = createServer(config ?? undefined);
36
+ const transport = new StdioServerTransport();
37
+ await server.connect(transport);
38
+
39
+ console.error('Google Calendar MCP Server running on stdio');
40
+ console.error(`OAuth: ${config ? '***configured***' : '(not configured — set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN)'}`);
41
+ console.error(`Tools available: ${TOOLS.length}`);
42
+ console.error('Ready for MCP client\n');
43
+ }
44
+
45
+ async function startHttp() {
46
+ const port = parseInt(process.env.PORT || '3000', 10);
47
+ const app = createMcpExpressApp({ host: '0.0.0.0' });
48
+
49
+ const transports: Record<string, StreamableHTTPServerTransport> = {};
50
+
51
+ app.post('/mcp', async (req: any, res: any) => {
52
+ const url = new URL(req.url, `http://${req.headers.host}`);
53
+ const qClientId = url.searchParams.get('GOOGLE_CLIENT_ID');
54
+ const qClientSecret = url.searchParams.get('GOOGLE_CLIENT_SECRET');
55
+ const qRefreshToken = url.searchParams.get('GOOGLE_REFRESH_TOKEN');
56
+ if (qClientId) process.env.GOOGLE_CLIENT_ID = qClientId;
57
+ if (qClientSecret) process.env.GOOGLE_CLIENT_SECRET = qClientSecret;
58
+ if (qRefreshToken) process.env.GOOGLE_REFRESH_TOKEN = qRefreshToken;
59
+
60
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
61
+
62
+ try {
63
+ let transport: StreamableHTTPServerTransport;
64
+
65
+ if (sessionId && transports[sessionId]) {
66
+ transport = transports[sessionId];
67
+ } else if (!sessionId && isInitializeRequest(req.body)) {
68
+ transport = new StreamableHTTPServerTransport({
69
+ sessionIdGenerator: () => randomUUID(),
70
+ onsessioninitialized: (sid: string) => {
71
+ transports[sid] = transport;
72
+ },
73
+ });
74
+
75
+ transport.onclose = () => {
76
+ const sid = transport.sessionId;
77
+ if (sid && transports[sid]) {
78
+ delete transports[sid];
79
+ }
80
+ };
81
+
82
+ const config = getConfig();
83
+ const server = createServer(config ?? undefined);
84
+ await server.connect(transport);
85
+ await transport.handleRequest(req, res, req.body);
86
+ return;
87
+ } else {
88
+ res.status(400).json({
89
+ jsonrpc: '2.0',
90
+ error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
91
+ id: null,
92
+ });
93
+ return;
94
+ }
95
+
96
+ await transport.handleRequest(req, res, req.body);
97
+ } catch (error) {
98
+ console.error('Error handling MCP request:', error);
99
+ if (!res.headersSent) {
100
+ res.status(500).json({
101
+ jsonrpc: '2.0',
102
+ error: { code: -32603, message: 'Internal server error' },
103
+ id: null,
104
+ });
105
+ }
106
+ }
107
+ });
108
+
109
+ app.get('/mcp', async (req: any, res: any) => {
110
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
111
+ if (!sessionId || !transports[sessionId]) {
112
+ res.status(400).send('Invalid or missing session ID');
113
+ return;
114
+ }
115
+ await transports[sessionId].handleRequest(req, res);
116
+ });
117
+
118
+ app.delete('/mcp', async (req: any, res: any) => {
119
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
120
+ if (!sessionId || !transports[sessionId]) {
121
+ res.status(400).send('Invalid or missing session ID');
122
+ return;
123
+ }
124
+ await transports[sessionId].handleRequest(req, res);
125
+ });
126
+
127
+ app.get('/', (_req: any, res: any) => {
128
+ res.json({
129
+ name: 'google-calendar-mcp',
130
+ version: '1.0.0',
131
+ status: 'ok',
132
+ tools: TOOLS.length,
133
+ transport: 'streamable-http',
134
+ endpoints: { mcp: '/mcp' },
135
+ });
136
+ });
137
+
138
+ const config = getConfig();
139
+ app.listen(port, () => {
140
+ console.log(`Google Calendar MCP Server (HTTP) listening on port ${port}`);
141
+ console.log(`OAuth: ${config ? '***configured***' : '(not configured — set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN)'}`);
142
+ console.log(`Tools available: ${TOOLS.length}`);
143
+ console.log(`MCP endpoint: http://localhost:${port}/mcp`);
144
+ });
145
+
146
+ process.on('SIGINT', async () => {
147
+ console.log('\nShutting down...');
148
+ for (const sessionId in transports) {
149
+ try {
150
+ await transports[sessionId].close();
151
+ delete transports[sessionId];
152
+ } catch { /* ignore */ }
153
+ }
154
+ process.exit(0);
155
+ });
156
+ }
157
+
158
+ async function main() {
159
+ const useHttp = process.argv.includes('--http');
160
+ if (useHttp) {
161
+ await startHttp();
162
+ } else {
163
+ await startStdio();
164
+ }
165
+ }
166
+
167
+ export default function createSmitheryServer(opts?: {
168
+ config?: {
169
+ GOOGLE_CLIENT_ID?: string;
170
+ GOOGLE_CLIENT_SECRET?: string;
171
+ GOOGLE_REFRESH_TOKEN?: string;
172
+ };
173
+ }) {
174
+ if (opts?.config?.GOOGLE_CLIENT_ID) process.env.GOOGLE_CLIENT_ID = opts.config.GOOGLE_CLIENT_ID;
175
+ if (opts?.config?.GOOGLE_CLIENT_SECRET) process.env.GOOGLE_CLIENT_SECRET = opts.config.GOOGLE_CLIENT_SECRET;
176
+ if (opts?.config?.GOOGLE_REFRESH_TOKEN) process.env.GOOGLE_REFRESH_TOKEN = opts.config.GOOGLE_REFRESH_TOKEN;
177
+ const config = getConfig();
178
+ return createServer(config ?? undefined);
179
+ }
180
+
181
+ main().catch((error) => {
182
+ console.error('Fatal error:', error);
183
+ process.exit(1);
184
+ });
package/src/server.ts ADDED
@@ -0,0 +1,447 @@
1
+ /**
2
+ * Shared MCP Server — used by both Node.js (index.ts) and CF Worker (worker.ts)
3
+ */
4
+
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
7
+ import { CalendarClient } from './calendar-client.js';
8
+ import { TOOLS } from './tools.js';
9
+
10
+ export interface CalendarMcpConfig {
11
+ clientId: string;
12
+ clientSecret: string;
13
+ refreshToken: string;
14
+ }
15
+
16
+ export function handleToolCall(
17
+ toolName: string,
18
+ args: Record<string, unknown>,
19
+ client: CalendarClient
20
+ ) {
21
+ switch (toolName) {
22
+ // ========== Events ==========
23
+ case 'gcal_list_events':
24
+ return client.listEvents({
25
+ calendarId: args.calendar_id as string,
26
+ timeMin: args.time_min as string | undefined,
27
+ timeMax: args.time_max as string | undefined,
28
+ q: args.q as string | undefined,
29
+ maxResults: args.max_results as number | undefined,
30
+ pageToken: args.page_token as string | undefined,
31
+ singleEvents: args.single_events as boolean | undefined,
32
+ orderBy: args.order_by as string | undefined,
33
+ timeZone: args.time_zone as string | undefined,
34
+ showDeleted: args.show_deleted as boolean | undefined,
35
+ });
36
+ case 'gcal_get_event':
37
+ return client.getEvent({
38
+ calendarId: args.calendar_id as string,
39
+ eventId: args.event_id as string,
40
+ timeZone: args.time_zone as string | undefined,
41
+ });
42
+ case 'gcal_create_event':
43
+ return client.createEvent({
44
+ calendarId: args.calendar_id as string,
45
+ summary: args.summary as string | undefined,
46
+ description: args.description as string | undefined,
47
+ location: args.location as string | undefined,
48
+ startDateTime: args.start_date_time as string | undefined,
49
+ startDate: args.start_date as string | undefined,
50
+ startTimeZone: args.start_time_zone as string | undefined,
51
+ endDateTime: args.end_date_time as string | undefined,
52
+ endDate: args.end_date as string | undefined,
53
+ endTimeZone: args.end_time_zone as string | undefined,
54
+ attendees: args.attendees as string[] | undefined,
55
+ recurrence: args.recurrence as string[] | undefined,
56
+ colorId: args.color_id as string | undefined,
57
+ visibility: args.visibility as string | undefined,
58
+ transparency: args.transparency as string | undefined,
59
+ sendUpdates: args.send_updates as string | undefined,
60
+ });
61
+ case 'gcal_update_event':
62
+ return client.updateEvent({
63
+ calendarId: args.calendar_id as string,
64
+ eventId: args.event_id as string,
65
+ summary: args.summary as string | undefined,
66
+ description: args.description as string | undefined,
67
+ location: args.location as string | undefined,
68
+ startDateTime: args.start_date_time as string | undefined,
69
+ startDate: args.start_date as string | undefined,
70
+ startTimeZone: args.start_time_zone as string | undefined,
71
+ endDateTime: args.end_date_time as string | undefined,
72
+ endDate: args.end_date as string | undefined,
73
+ endTimeZone: args.end_time_zone as string | undefined,
74
+ attendees: args.attendees as string[] | undefined,
75
+ recurrence: args.recurrence as string[] | undefined,
76
+ colorId: args.color_id as string | undefined,
77
+ visibility: args.visibility as string | undefined,
78
+ transparency: args.transparency as string | undefined,
79
+ sendUpdates: args.send_updates as string | undefined,
80
+ });
81
+ case 'gcal_patch_event':
82
+ return client.patchEvent({
83
+ calendarId: args.calendar_id as string,
84
+ eventId: args.event_id as string,
85
+ summary: args.summary as string | undefined,
86
+ description: args.description as string | undefined,
87
+ location: args.location as string | undefined,
88
+ startDateTime: args.start_date_time as string | undefined,
89
+ startDate: args.start_date as string | undefined,
90
+ startTimeZone: args.start_time_zone as string | undefined,
91
+ endDateTime: args.end_date_time as string | undefined,
92
+ endDate: args.end_date as string | undefined,
93
+ endTimeZone: args.end_time_zone as string | undefined,
94
+ attendees: args.attendees as string[] | undefined,
95
+ colorId: args.color_id as string | undefined,
96
+ visibility: args.visibility as string | undefined,
97
+ transparency: args.transparency as string | undefined,
98
+ sendUpdates: args.send_updates as string | undefined,
99
+ });
100
+ case 'gcal_delete_event':
101
+ return client.deleteEvent({
102
+ calendarId: args.calendar_id as string,
103
+ eventId: args.event_id as string,
104
+ sendUpdates: args.send_updates as string | undefined,
105
+ });
106
+ case 'gcal_quick_add':
107
+ return client.quickAdd({
108
+ calendarId: args.calendar_id as string,
109
+ text: args.text as string,
110
+ sendUpdates: args.send_updates as string | undefined,
111
+ });
112
+ case 'gcal_move_event':
113
+ return client.moveEvent({
114
+ calendarId: args.calendar_id as string,
115
+ eventId: args.event_id as string,
116
+ destination: args.destination as string,
117
+ sendUpdates: args.send_updates as string | undefined,
118
+ });
119
+ case 'gcal_list_instances':
120
+ return client.listInstances({
121
+ calendarId: args.calendar_id as string,
122
+ eventId: args.event_id as string,
123
+ timeMin: args.time_min as string | undefined,
124
+ timeMax: args.time_max as string | undefined,
125
+ maxResults: args.max_results as number | undefined,
126
+ pageToken: args.page_token as string | undefined,
127
+ timeZone: args.time_zone as string | undefined,
128
+ });
129
+ case 'gcal_import_event':
130
+ return client.importEvent({
131
+ calendarId: args.calendar_id as string,
132
+ iCalUID: args.ical_uid as string,
133
+ summary: args.summary as string | undefined,
134
+ description: args.description as string | undefined,
135
+ location: args.location as string | undefined,
136
+ startDateTime: args.start_date_time as string | undefined,
137
+ startDate: args.start_date as string | undefined,
138
+ startTimeZone: args.start_time_zone as string | undefined,
139
+ endDateTime: args.end_date_time as string | undefined,
140
+ endDate: args.end_date as string | undefined,
141
+ endTimeZone: args.end_time_zone as string | undefined,
142
+ });
143
+
144
+ // ========== CalendarList ==========
145
+ case 'gcal_list_calendars':
146
+ return client.listCalendars({
147
+ maxResults: args.max_results as number | undefined,
148
+ pageToken: args.page_token as string | undefined,
149
+ showDeleted: args.show_deleted as boolean | undefined,
150
+ showHidden: args.show_hidden as boolean | undefined,
151
+ });
152
+ case 'gcal_get_calendar_entry':
153
+ return client.getCalendarEntry({
154
+ calendarId: args.calendar_id as string,
155
+ });
156
+ case 'gcal_add_calendar':
157
+ return client.addCalendar({
158
+ id: args.id as string,
159
+ colorId: args.color_id as string | undefined,
160
+ summaryOverride: args.summary_override as string | undefined,
161
+ hidden: args.hidden as boolean | undefined,
162
+ selected: args.selected as boolean | undefined,
163
+ });
164
+ case 'gcal_update_calendar_entry':
165
+ return client.updateCalendarEntry({
166
+ calendarId: args.calendar_id as string,
167
+ colorId: args.color_id as string | undefined,
168
+ summaryOverride: args.summary_override as string | undefined,
169
+ hidden: args.hidden as boolean | undefined,
170
+ selected: args.selected as boolean | undefined,
171
+ defaultReminders: args.default_reminders as { method: string; minutes: number }[] | undefined,
172
+ });
173
+ case 'gcal_remove_calendar':
174
+ return client.removeCalendar({
175
+ calendarId: args.calendar_id as string,
176
+ });
177
+
178
+ // ========== Calendars ==========
179
+ case 'gcal_get_calendar':
180
+ return client.getCalendar({
181
+ calendarId: args.calendar_id as string,
182
+ });
183
+ case 'gcal_create_calendar':
184
+ return client.createCalendar({
185
+ summary: args.summary as string,
186
+ description: args.description as string | undefined,
187
+ location: args.location as string | undefined,
188
+ timeZone: args.time_zone as string | undefined,
189
+ });
190
+ case 'gcal_update_calendar':
191
+ return client.updateCalendar({
192
+ calendarId: args.calendar_id as string,
193
+ summary: args.summary as string | undefined,
194
+ description: args.description as string | undefined,
195
+ location: args.location as string | undefined,
196
+ timeZone: args.time_zone as string | undefined,
197
+ });
198
+ case 'gcal_delete_calendar':
199
+ return client.deleteCalendar({
200
+ calendarId: args.calendar_id as string,
201
+ });
202
+ case 'gcal_clear_calendar':
203
+ return client.clearCalendar({
204
+ calendarId: args.calendar_id as string,
205
+ });
206
+
207
+ // ========== ACL ==========
208
+ case 'gcal_list_acl':
209
+ return client.listAcl({
210
+ calendarId: args.calendar_id as string,
211
+ maxResults: args.max_results as number | undefined,
212
+ pageToken: args.page_token as string | undefined,
213
+ showDeleted: args.show_deleted as boolean | undefined,
214
+ });
215
+ case 'gcal_get_acl':
216
+ return client.getAcl({
217
+ calendarId: args.calendar_id as string,
218
+ ruleId: args.rule_id as string,
219
+ });
220
+ case 'gcal_create_acl':
221
+ return client.createAcl({
222
+ calendarId: args.calendar_id as string,
223
+ role: args.role as string,
224
+ scopeType: args.scope_type as string,
225
+ scopeValue: args.scope_value as string | undefined,
226
+ sendNotifications: args.send_notifications as boolean | undefined,
227
+ });
228
+ case 'gcal_update_acl':
229
+ return client.updateAcl({
230
+ calendarId: args.calendar_id as string,
231
+ ruleId: args.rule_id as string,
232
+ role: args.role as string,
233
+ sendNotifications: args.send_notifications as boolean | undefined,
234
+ });
235
+ case 'gcal_delete_acl':
236
+ return client.deleteAcl({
237
+ calendarId: args.calendar_id as string,
238
+ ruleId: args.rule_id as string,
239
+ });
240
+
241
+ // ========== Utility ==========
242
+ case 'gcal_query_freebusy':
243
+ return client.queryFreeBusy({
244
+ timeMin: args.time_min as string,
245
+ timeMax: args.time_max as string,
246
+ timeZone: args.time_zone as string | undefined,
247
+ calendarIds: args.calendar_ids as string[],
248
+ });
249
+ case 'gcal_get_colors':
250
+ return client.getColors();
251
+ case 'gcal_list_settings':
252
+ return client.listSettings({
253
+ maxResults: args.max_results as number | undefined,
254
+ pageToken: args.page_token as string | undefined,
255
+ });
256
+
257
+ default:
258
+ throw new Error(`Unknown tool: ${toolName}`);
259
+ }
260
+ }
261
+
262
+ export function createServer(config?: CalendarMcpConfig) {
263
+ const server = new McpServer({
264
+ name: 'google-calendar-mcp',
265
+ version: '1.0.0',
266
+ });
267
+
268
+ let client: CalendarClient | null = null;
269
+
270
+ for (const tool of TOOLS) {
271
+ server.registerTool(
272
+ tool.name,
273
+ {
274
+ description: tool.description,
275
+ inputSchema: tool.inputSchema as any,
276
+ annotations: tool.annotations,
277
+ },
278
+ async (args: Record<string, unknown>) => {
279
+ const clientId =
280
+ config?.clientId ||
281
+ (args as Record<string, unknown>).GOOGLE_CLIENT_ID as string;
282
+ const clientSecret =
283
+ config?.clientSecret ||
284
+ (args as Record<string, unknown>).GOOGLE_CLIENT_SECRET as string;
285
+ const refreshToken =
286
+ config?.refreshToken ||
287
+ (args as Record<string, unknown>).GOOGLE_REFRESH_TOKEN as string;
288
+
289
+ if (!clientId || !clientSecret || !refreshToken) {
290
+ return {
291
+ content: [{ type: 'text' as const, text: 'Error: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REFRESH_TOKEN are all required.' }],
292
+ isError: true,
293
+ };
294
+ }
295
+
296
+ if (!client || config?.clientId !== clientId) {
297
+ client = new CalendarClient({ clientId, clientSecret, refreshToken });
298
+ }
299
+
300
+ try {
301
+ const result = await handleToolCall(tool.name, args, client);
302
+ const text = result === undefined ? '{"success": true}' : JSON.stringify(result, null, 2);
303
+ return {
304
+ content: [{ type: 'text' as const, text }],
305
+ isError: false,
306
+ };
307
+ } catch (error) {
308
+ return {
309
+ content: [{ type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
310
+ isError: true,
311
+ };
312
+ }
313
+ }
314
+ );
315
+ }
316
+
317
+ // Register prompts
318
+ server.prompt(
319
+ 'schedule-and-manage',
320
+ 'Guide for creating events, recurring schedules, attendees, and reminders',
321
+ async () => ({
322
+ messages: [{
323
+ role: 'user' as const,
324
+ content: {
325
+ type: 'text' as const,
326
+ text: [
327
+ 'You are a Google Calendar scheduling assistant.',
328
+ '',
329
+ 'Creating events:',
330
+ '1. **Timed event** — gcal_create_event with start_date_time + end_date_time (RFC 3339)',
331
+ '2. **All-day event** — Use start_date + end_date (YYYY-MM-DD). Single day: start=15, end=16',
332
+ '3. **Quick add** — gcal_quick_add with natural language ("Meeting tomorrow 3pm")',
333
+ '4. **Attendees** — Pass email addresses array, set send_updates="all" to notify',
334
+ '',
335
+ 'Recurring events:',
336
+ '1. **Daily** — recurrence: ["RRULE:FREQ=DAILY"]',
337
+ '2. **Weekly** — recurrence: ["RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR"]',
338
+ '3. **Monthly** — recurrence: ["RRULE:FREQ=MONTHLY;BYMONTHDAY=15"]',
339
+ '4. **Until date** — Add ;UNTIL=20261231T235959Z',
340
+ '5. **Count** — Add ;COUNT=10 for 10 occurrences',
341
+ '6. **Exceptions** — Add EXDATE entries to skip dates',
342
+ '7. **View instances** — gcal_list_instances to see individual occurrences',
343
+ '',
344
+ 'Reminders:',
345
+ '- Default reminders: { "useDefault": true }',
346
+ '- Custom: { "useDefault": false, "overrides": [{ "method": "popup", "minutes": 10 }, { "method": "email", "minutes": 60 }] }',
347
+ '',
348
+ 'Tips:',
349
+ '- Use "primary" as calendar_id for the main calendar',
350
+ '- Set transparency="transparent" for events that don\'t block time',
351
+ '- Use gcal_patch_event to change only specific fields without affecting others',
352
+ '- Use gcal_move_event to transfer events between calendars',
353
+ ].join('\n'),
354
+ },
355
+ }],
356
+ }),
357
+ );
358
+
359
+ server.prompt(
360
+ 'search-and-organize',
361
+ 'Guide for searching events, managing calendars, sharing, and checking availability',
362
+ async () => ({
363
+ messages: [{
364
+ role: 'user' as const,
365
+ content: {
366
+ type: 'text' as const,
367
+ text: [
368
+ 'You are a Google Calendar organization assistant.',
369
+ '',
370
+ 'Searching events:',
371
+ '- **By text** — q parameter searches summary, description, location, attendees',
372
+ '- **By time** — time_min + time_max (RFC 3339) to filter date range',
373
+ '- **Expand recurring** — single_events=true to see individual instances',
374
+ '- **Sort** — order_by="startTime" (requires singleEvents) or "updated"',
375
+ '- **Pagination** — Use max_results + page_token for large result sets',
376
+ '',
377
+ 'Managing calendars:',
378
+ '1. **List all** — gcal_list_calendars to see subscribed calendars',
379
+ '2. **Create** — gcal_create_calendar for a new secondary calendar',
380
+ '3. **Customize** — gcal_update_calendar_entry for display color, name override',
381
+ '4. **Subscribe** — gcal_add_calendar to add a shared calendar',
382
+ '5. **Unsubscribe** — gcal_remove_calendar (doesn\'t delete the calendar)',
383
+ '',
384
+ 'Sharing calendars (ACL):',
385
+ '- **freeBusyReader** — Can only see free/busy',
386
+ '- **reader** — Can see event details',
387
+ '- **writer** — Can create and edit events',
388
+ '- **owner** — Full control including sharing',
389
+ '- scope_type: "user" (email), "group" (group email), "domain", or "default" (public)',
390
+ '',
391
+ 'Checking availability:',
392
+ '- gcal_query_freebusy with time range and calendar IDs',
393
+ '- Returns busy time blocks for each calendar',
394
+ '- Use to find free slots for scheduling meetings',
395
+ '',
396
+ 'Colors:',
397
+ '- gcal_get_colors for color palette (IDs 1-24 for calendars, 1-11 for events)',
398
+ '- Apply to events: color_id in gcal_create_event',
399
+ '- Apply to calendars: color_id in gcal_update_calendar_entry',
400
+ ].join('\n'),
401
+ },
402
+ }],
403
+ }),
404
+ );
405
+
406
+ // Register resource
407
+ server.resource(
408
+ 'server-info',
409
+ 'gcal://server-info',
410
+ {
411
+ description: 'Connection status and available tools for this Google Calendar MCP server',
412
+ mimeType: 'application/json',
413
+ },
414
+ async () => ({
415
+ contents: [{
416
+ uri: 'gcal://server-info',
417
+ mimeType: 'application/json',
418
+ text: JSON.stringify({
419
+ name: 'google-calendar-mcp',
420
+ version: '1.0.0',
421
+ connected: !!config,
422
+ has_oauth: !!(config?.clientId),
423
+ tools_available: TOOLS.length,
424
+ tool_categories: {
425
+ events: 10,
426
+ calendar_list: 5,
427
+ calendars: 5,
428
+ acl: 5,
429
+ utility: 3,
430
+ },
431
+ }, null, 2),
432
+ }],
433
+ }),
434
+ );
435
+
436
+ // Override tools/list handler to return raw JSON Schema with property descriptions
437
+ (server as any).server.setRequestHandler(ListToolsRequestSchema, () => ({
438
+ tools: TOOLS.map(tool => ({
439
+ name: tool.name,
440
+ description: tool.description,
441
+ inputSchema: tool.inputSchema,
442
+ annotations: tool.annotations,
443
+ })),
444
+ }));
445
+
446
+ return server;
447
+ }