google-calendar-workspace-mcp-server 0.0.7 → 0.0.9

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.
@@ -33,7 +33,7 @@ class MockCalendarClient {
33
33
  timeZone: 'America/New_York',
34
34
  },
35
35
  status: 'confirmed',
36
- htmlLink: 'https://calendar.google.com/event?eid=event1',
36
+ htmlLink: 'https://www.google.com/calendar/event?eid=ZXZlbnQxX2VpZA',
37
37
  attendees: [
38
38
  {
39
39
  email: 'user1@example.com',
@@ -55,7 +55,7 @@ class MockCalendarClient {
55
55
  date: '2024-01-17',
56
56
  },
57
57
  status: 'confirmed',
58
- htmlLink: 'https://calendar.google.com/event?eid=event2',
58
+ htmlLink: 'https://www.google.com/calendar/event?eid=ZXZlbnQyX2VpZA',
59
59
  },
60
60
  ];
61
61
  const filteredEvents = options?.q
@@ -87,7 +87,7 @@ class MockCalendarClient {
87
87
  timeZone: 'America/New_York',
88
88
  },
89
89
  status: 'confirmed',
90
- htmlLink: 'https://calendar.google.com/event?eid=event1',
90
+ htmlLink: 'https://www.google.com/calendar/event?eid=ZXZlbnQxX2VpZA',
91
91
  created: '2024-01-01T00:00:00Z',
92
92
  updated: '2024-01-01T00:00:00Z',
93
93
  creator: {
@@ -132,7 +132,7 @@ class MockCalendarClient {
132
132
  start: event.start || { dateTime: '2024-01-20T10:00:00-05:00' },
133
133
  end: event.end || { dateTime: '2024-01-20T11:00:00-05:00' },
134
134
  status: 'confirmed',
135
- htmlLink: 'https://calendar.google.com/event?eid=new-event-id',
135
+ htmlLink: 'https://www.google.com/calendar/event?eid=bmV3X2V2ZW50X2VpZA',
136
136
  created: new Date().toISOString(),
137
137
  updated: new Date().toISOString(),
138
138
  attendees: event.attendees,
@@ -150,7 +150,7 @@ class MockCalendarClient {
150
150
  start: event.start || { dateTime: '2024-01-20T10:00:00-05:00' },
151
151
  end: event.end || { dateTime: '2024-01-20T11:00:00-05:00' },
152
152
  status: 'confirmed',
153
- htmlLink: `https://calendar.google.com/event?eid=${eventId}`,
153
+ htmlLink: `https://www.google.com/calendar/event?eid=dXBkYXRlZF8${eventId}`,
154
154
  created: '2024-01-01T00:00:00Z',
155
155
  updated: new Date().toISOString(),
156
156
  attendees: event.attendees,
@@ -194,6 +194,9 @@ class MockCalendarClient {
194
194
  ],
195
195
  };
196
196
  }
197
+ async getAccountEmail() {
198
+ return 'me@example.com';
199
+ }
197
200
  async queryFreebusy(request) {
198
201
  return {
199
202
  kind: 'calendar#freeBusy',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "google-calendar-workspace-mcp-server",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "MCP server for Google Calendar integration with service account support",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  "stage-publish": "npm version"
30
30
  },
31
31
  "dependencies": {
32
- "@modelcontextprotocol/sdk": "^1.19.1",
32
+ "@modelcontextprotocol/sdk": "^1.29.0",
33
33
  "google-auth-library": "^10.5.0",
34
34
  "zod": "^3.24.1"
35
35
  },
@@ -52,6 +52,13 @@ export interface ICalendarClient {
52
52
  * Query free/busy information
53
53
  */
54
54
  queryFreebusy(request: FreeBusyRequest): Promise<FreeBusyResponse>;
55
+ /**
56
+ * Get the email address of the Google account this client reads from.
57
+ * Used to construct account-scoped Calendar web URLs so that event links
58
+ * open in the correct account regardless of which accounts the reader
59
+ * happens to be signed into in their browser.
60
+ */
61
+ getAccountEmail(): Promise<string>;
55
62
  }
56
63
  /**
57
64
  * Service account credentials structure
@@ -106,6 +113,7 @@ export declare class ServiceAccountCalendarClient implements ICalendarClient {
106
113
  pageToken?: string;
107
114
  }): Promise<CalendarList>;
108
115
  queryFreebusy(request: FreeBusyRequest): Promise<FreeBusyResponse>;
116
+ getAccountEmail(): Promise<string>;
109
117
  }
110
118
  export type ClientFactory = () => ICalendarClient;
111
119
  /**
package/shared/server.js CHANGED
@@ -84,6 +84,9 @@ export class ServiceAccountCalendarClient {
84
84
  const { queryFreebusy } = await import('./calendar-client/lib/query-freebusy.js');
85
85
  return queryFreebusy(this.baseUrl, headers, request);
86
86
  }
87
+ async getAccountEmail() {
88
+ return this.impersonateEmail;
89
+ }
87
90
  }
88
91
  /**
89
92
  * Creates the default Google Calendar client based on environment variables.
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { logError } from '../logging.js';
3
+ import { buildCalendarEventUrl } from '../utils/calendar-helpers.js';
3
4
  export const CreateEventSchema = z.object({
4
5
  calendar_id: z
5
6
  .string()
@@ -166,7 +167,10 @@ export function createEventTool(server, clientFactory) {
166
167
  }
167
168
  // Determine if we need supportsAttachments parameter
168
169
  const hasAttachments = parsed.attachments && parsed.attachments.length > 0;
169
- const result = await client.createEvent(parsed.calendar_id, event, hasAttachments ? { supportsAttachments: true } : undefined);
170
+ const [result, accountEmail] = await Promise.all([
171
+ client.createEvent(parsed.calendar_id, event, hasAttachments ? { supportsAttachments: true } : undefined),
172
+ client.getAccountEmail(),
173
+ ]);
170
174
  let output = `# Event Created Successfully\n\n`;
171
175
  output += `## ${result.summary || '(No title)'}\n\n`;
172
176
  output += `**Event ID:** ${result.id}\n`;
@@ -214,9 +218,10 @@ export function createEventTool(server, clientFactory) {
214
218
  output += ` - ${attachment.title || attachment.fileUrl}\n`;
215
219
  }
216
220
  }
217
- // Link
218
- if (result.htmlLink) {
219
- output += `\n**Event Link:** ${result.htmlLink}\n`;
221
+ // Link — account-scoped so it opens in the correct mailbox/calendar.
222
+ const eventUrl = buildCalendarEventUrl(accountEmail, result.htmlLink);
223
+ if (eventUrl) {
224
+ output += `\n**Event Link:** ${eventUrl}\n`;
220
225
  }
221
226
  return {
222
227
  content: [
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { logError } from '../logging.js';
3
+ import { buildCalendarEventUrl } from '../utils/calendar-helpers.js';
3
4
  export const GetEventSchema = z.object({
4
5
  calendar_id: z
5
6
  .string()
@@ -32,7 +33,10 @@ export function getEventTool(server, clientFactory) {
32
33
  try {
33
34
  const parsed = GetEventSchema.parse(args);
34
35
  const client = clientFactory();
35
- const event = await client.getEvent(parsed.calendar_id, parsed.event_id);
36
+ const [event, accountEmail] = await Promise.all([
37
+ client.getEvent(parsed.calendar_id, parsed.event_id),
38
+ client.getAccountEmail(),
39
+ ]);
36
40
  let output = `# Event Details\n\n`;
37
41
  output += `## ${event.summary || '(No title)'}\n\n`;
38
42
  output += `**Event ID:** ${event.id}\n`;
@@ -128,9 +132,10 @@ export function getEventTool(server, clientFactory) {
128
132
  if (event.updated) {
129
133
  output += `**Updated:** ${new Date(event.updated).toLocaleString()}\n`;
130
134
  }
131
- // Link
132
- if (event.htmlLink) {
133
- output += `\n**Event Link:** ${event.htmlLink}\n`;
135
+ // Link — account-scoped so it opens in the correct mailbox/calendar.
136
+ const eventUrl = buildCalendarEventUrl(accountEmail, event.htmlLink);
137
+ if (eventUrl) {
138
+ output += `\n**Event Link:** ${eventUrl}\n`;
134
139
  }
135
140
  return {
136
141
  content: [
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { logError } from '../logging.js';
3
+ import { buildCalendarEventUrl } from '../utils/calendar-helpers.js';
3
4
  export const ListEventsSchema = z.object({
4
5
  calendar_id: z
5
6
  .string()
@@ -77,14 +78,17 @@ export function listEventsTool(server, clientFactory) {
77
78
  try {
78
79
  const parsed = ListEventsSchema.parse(args);
79
80
  const client = clientFactory();
80
- const result = await client.listEvents(parsed.calendar_id, {
81
- timeMin: parsed.time_min,
82
- timeMax: parsed.time_max,
83
- maxResults: parsed.max_results,
84
- q: parsed.query,
85
- singleEvents: parsed.single_events,
86
- orderBy: parsed.order_by,
87
- });
81
+ const [result, accountEmail] = await Promise.all([
82
+ client.listEvents(parsed.calendar_id, {
83
+ timeMin: parsed.time_min,
84
+ timeMax: parsed.time_max,
85
+ maxResults: parsed.max_results,
86
+ q: parsed.query,
87
+ singleEvents: parsed.single_events,
88
+ orderBy: parsed.order_by,
89
+ }),
90
+ client.getAccountEmail(),
91
+ ]);
88
92
  const events = result.items || [];
89
93
  if (events.length === 0) {
90
94
  return {
@@ -156,9 +160,10 @@ export function listEventsTool(server, clientFactory) {
156
160
  : event.description;
157
161
  output += `**Description:** ${truncated}\n`;
158
162
  }
159
- // Link
160
- if (event.htmlLink) {
161
- output += `**Link:** ${event.htmlLink}\n`;
163
+ // Link — account-scoped so it opens in the correct mailbox/calendar.
164
+ const eventUrl = buildCalendarEventUrl(accountEmail, event.htmlLink);
165
+ if (eventUrl) {
166
+ output += `**Link:** ${eventUrl}\n`;
162
167
  }
163
168
  output += '\n---\n\n';
164
169
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { logError } from '../logging.js';
3
+ import { buildCalendarEventUrl } from '../utils/calendar-helpers.js';
3
4
  export const UpdateEventSchema = z.object({
4
5
  event_id: z.string().min(1).describe('The ID of the event to update.'),
5
6
  calendar_id: z
@@ -190,7 +191,10 @@ export function updateEventTool(server, clientFactory) {
190
191
  if (hasAttachments) {
191
192
  options.supportsAttachments = true;
192
193
  }
193
- const result = await client.updateEvent(parsed.calendar_id, parsed.event_id, eventUpdate, Object.keys(options).length > 0 ? options : undefined);
194
+ const [result, accountEmail] = await Promise.all([
195
+ client.updateEvent(parsed.calendar_id, parsed.event_id, eventUpdate, Object.keys(options).length > 0 ? options : undefined),
196
+ client.getAccountEmail(),
197
+ ]);
194
198
  let output = `# Event Updated Successfully\n\n`;
195
199
  output += `## ${result.summary || '(No title)'}\n\n`;
196
200
  output += `**Event ID:** ${result.id}\n`;
@@ -238,9 +242,10 @@ export function updateEventTool(server, clientFactory) {
238
242
  output += ` - ${attachment.title || attachment.fileUrl}\n`;
239
243
  }
240
244
  }
241
- // Link
242
- if (result.htmlLink) {
243
- output += `\n**Event Link:** ${result.htmlLink}\n`;
245
+ // Link — account-scoped so it opens in the correct mailbox/calendar.
246
+ const eventUrl = buildCalendarEventUrl(accountEmail, result.htmlLink);
247
+ if (eventUrl) {
248
+ output += `\n**Event Link:** ${eventUrl}\n`;
244
249
  }
245
250
  return {
246
251
  content: [
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Builds an account-scoped Google Calendar event URL.
3
+ *
4
+ * Uses the `/calendar/u/<account-email>/r/eventedit/<eid>` path form so the
5
+ * link opens in the correct account regardless of which Google accounts the
6
+ * reader is signed into in their browser. The default `htmlLink` returned by
7
+ * the Calendar API (`https://www.google.com/calendar/event?eid=...`) carries
8
+ * no account context, so Google's web UI guesses based on the reader's
9
+ * browser session and 404s when the guess is wrong.
10
+ *
11
+ * The `<eid>` segment is the opaque base64-style identifier from the
12
+ * `?eid=...` query parameter on `htmlLink` — NOT the raw `event.id`. Using
13
+ * the raw event id (e.g. `p1qvrkvfpl6d3a4rr6careb3bk`) yields a 500 page.
14
+ *
15
+ * Falls back to the original `htmlLink` if the input cannot be parsed (e.g.
16
+ * the API response omitted `htmlLink` or it lacks an `eid` parameter).
17
+ */
18
+ export declare function buildCalendarEventUrl(accountEmail: string, htmlLink: string | undefined): string | undefined;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Builds an account-scoped Google Calendar event URL.
3
+ *
4
+ * Uses the `/calendar/u/<account-email>/r/eventedit/<eid>` path form so the
5
+ * link opens in the correct account regardless of which Google accounts the
6
+ * reader is signed into in their browser. The default `htmlLink` returned by
7
+ * the Calendar API (`https://www.google.com/calendar/event?eid=...`) carries
8
+ * no account context, so Google's web UI guesses based on the reader's
9
+ * browser session and 404s when the guess is wrong.
10
+ *
11
+ * The `<eid>` segment is the opaque base64-style identifier from the
12
+ * `?eid=...` query parameter on `htmlLink` — NOT the raw `event.id`. Using
13
+ * the raw event id (e.g. `p1qvrkvfpl6d3a4rr6careb3bk`) yields a 500 page.
14
+ *
15
+ * Falls back to the original `htmlLink` if the input cannot be parsed (e.g.
16
+ * the API response omitted `htmlLink` or it lacks an `eid` parameter).
17
+ */
18
+ export function buildCalendarEventUrl(accountEmail, htmlLink) {
19
+ if (!htmlLink) {
20
+ return undefined;
21
+ }
22
+ const eid = extractEid(htmlLink);
23
+ if (!eid) {
24
+ return htmlLink;
25
+ }
26
+ return `https://calendar.google.com/calendar/u/${encodeURIComponent(accountEmail)}/r/eventedit/${eid}`;
27
+ }
28
+ function extractEid(htmlLink) {
29
+ try {
30
+ const url = new URL(htmlLink);
31
+ return url.searchParams.get('eid') ?? undefined;
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }