google-calendar-workspace-mcp-server 0.0.11 → 0.0.13
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/build/index.integration-with-mock.js +3 -0
- package/package.json +3 -3
- package/shared/server.d.ts +11 -0
- package/shared/server.js +5 -0
- package/shared/tools/create-event.js +6 -3
- package/shared/tools/get-event.js +6 -3
- package/shared/tools/list-events.js +13 -10
- package/shared/tools/update-event.js +6 -3
- package/shared/utils/calendar-helpers.d.ts +18 -13
- package/shared/utils/calendar-helpers.js +23 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google-calendar-workspace-mcp-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "MCP server for Google Calendar integration with service account support",
|
|
5
5
|
"mcpName": "com.pulsemcp/google-calendar",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
34
34
|
"google-auth-library": "^10.5.0",
|
|
35
|
-
"zod": "^3.
|
|
35
|
+
"zod": "^3.25.76"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^22.10.6",
|
|
39
|
-
"tsx": "^4.
|
|
39
|
+
"tsx": "^4.22.4",
|
|
40
40
|
"typescript": "^5.7.3"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
package/shared/server.d.ts
CHANGED
|
@@ -52,6 +52,15 @@ 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 append `&authuser=<email>` to event URLs so they open against
|
|
58
|
+
* the correct account in multi-account browsers (the reader's default
|
|
59
|
+
* Google account may not be the calendar owner). Returns an empty string
|
|
60
|
+
* if the account email cannot be resolved — callers should treat empty
|
|
61
|
+
* as "skip authuser" and fall back to the bare `?eid=` form.
|
|
62
|
+
*/
|
|
63
|
+
getAccountEmail(): Promise<string>;
|
|
55
64
|
}
|
|
56
65
|
/**
|
|
57
66
|
* Service account credentials structure
|
|
@@ -72,6 +81,7 @@ export interface ServiceAccountCredentials {
|
|
|
72
81
|
* Google Calendar API client implementation using service account with domain-wide delegation
|
|
73
82
|
*/
|
|
74
83
|
export declare class ServiceAccountCalendarClient implements ICalendarClient {
|
|
84
|
+
private impersonateEmail;
|
|
75
85
|
private baseUrl;
|
|
76
86
|
private jwtClient;
|
|
77
87
|
private cachedToken;
|
|
@@ -105,6 +115,7 @@ export declare class ServiceAccountCalendarClient implements ICalendarClient {
|
|
|
105
115
|
pageToken?: string;
|
|
106
116
|
}): Promise<CalendarList>;
|
|
107
117
|
queryFreebusy(request: FreeBusyRequest): Promise<FreeBusyResponse>;
|
|
118
|
+
getAccountEmail(): Promise<string>;
|
|
108
119
|
}
|
|
109
120
|
export type ClientFactory = () => ICalendarClient;
|
|
110
121
|
/**
|
package/shared/server.js
CHANGED
|
@@ -5,12 +5,14 @@ import { createRegisterTools, parseEnabledToolGroups } from './tools.js';
|
|
|
5
5
|
* Google Calendar API client implementation using service account with domain-wide delegation
|
|
6
6
|
*/
|
|
7
7
|
export class ServiceAccountCalendarClient {
|
|
8
|
+
impersonateEmail;
|
|
8
9
|
baseUrl = 'https://www.googleapis.com/calendar/v3';
|
|
9
10
|
jwtClient;
|
|
10
11
|
cachedToken = null;
|
|
11
12
|
tokenExpiry = 0;
|
|
12
13
|
refreshPromise = null;
|
|
13
14
|
constructor(credentials, impersonateEmail) {
|
|
15
|
+
this.impersonateEmail = impersonateEmail;
|
|
14
16
|
this.jwtClient = new JWT({
|
|
15
17
|
email: credentials.client_email,
|
|
16
18
|
key: credentials.private_key,
|
|
@@ -82,6 +84,9 @@ export class ServiceAccountCalendarClient {
|
|
|
82
84
|
const { queryFreebusy } = await import('./calendar-client/lib/query-freebusy.js');
|
|
83
85
|
return queryFreebusy(this.baseUrl, headers, request);
|
|
84
86
|
}
|
|
87
|
+
async getAccountEmail() {
|
|
88
|
+
return this.impersonateEmail;
|
|
89
|
+
}
|
|
85
90
|
}
|
|
86
91
|
/**
|
|
87
92
|
* Creates the default Google Calendar client based on environment variables.
|
|
@@ -167,7 +167,10 @@ export function createEventTool(server, clientFactory) {
|
|
|
167
167
|
}
|
|
168
168
|
// Determine if we need supportsAttachments parameter
|
|
169
169
|
const hasAttachments = parsed.attachments && parsed.attachments.length > 0;
|
|
170
|
-
const result = await
|
|
170
|
+
const [result, accountEmail] = await Promise.all([
|
|
171
|
+
client.createEvent(parsed.calendar_id, event, hasAttachments ? { supportsAttachments: true } : undefined),
|
|
172
|
+
client.getAccountEmail(),
|
|
173
|
+
]);
|
|
171
174
|
let output = `# Event Created Successfully\n\n`;
|
|
172
175
|
output += `## ${result.summary || '(No title)'}\n\n`;
|
|
173
176
|
output += `**Event ID:** ${result.id}\n`;
|
|
@@ -215,8 +218,8 @@ export function createEventTool(server, clientFactory) {
|
|
|
215
218
|
output += ` - ${attachment.title || attachment.fileUrl}\n`;
|
|
216
219
|
}
|
|
217
220
|
}
|
|
218
|
-
// Link —
|
|
219
|
-
const eventUrl = buildCalendarEventUrl(result.htmlLink);
|
|
221
|
+
// Link — `?eid=&authuser=<email>` form so multi-account readers don't 404.
|
|
222
|
+
const eventUrl = buildCalendarEventUrl(result.htmlLink, accountEmail);
|
|
220
223
|
if (eventUrl) {
|
|
221
224
|
output += `\n**Event Link:** ${eventUrl}\n`;
|
|
222
225
|
}
|
|
@@ -33,7 +33,10 @@ export function getEventTool(server, clientFactory) {
|
|
|
33
33
|
try {
|
|
34
34
|
const parsed = GetEventSchema.parse(args);
|
|
35
35
|
const client = clientFactory();
|
|
36
|
-
const event = await
|
|
36
|
+
const [event, accountEmail] = await Promise.all([
|
|
37
|
+
client.getEvent(parsed.calendar_id, parsed.event_id),
|
|
38
|
+
client.getAccountEmail(),
|
|
39
|
+
]);
|
|
37
40
|
let output = `# Event Details\n\n`;
|
|
38
41
|
output += `## ${event.summary || '(No title)'}\n\n`;
|
|
39
42
|
output += `**Event ID:** ${event.id}\n`;
|
|
@@ -129,8 +132,8 @@ export function getEventTool(server, clientFactory) {
|
|
|
129
132
|
if (event.updated) {
|
|
130
133
|
output += `**Updated:** ${new Date(event.updated).toLocaleString()}\n`;
|
|
131
134
|
}
|
|
132
|
-
// Link —
|
|
133
|
-
const eventUrl = buildCalendarEventUrl(event.htmlLink);
|
|
135
|
+
// Link — `?eid=&authuser=<email>` form so multi-account readers don't 404.
|
|
136
|
+
const eventUrl = buildCalendarEventUrl(event.htmlLink, accountEmail);
|
|
134
137
|
if (eventUrl) {
|
|
135
138
|
output += `\n**Event Link:** ${eventUrl}\n`;
|
|
136
139
|
}
|
|
@@ -78,14 +78,17 @@ export function listEventsTool(server, clientFactory) {
|
|
|
78
78
|
try {
|
|
79
79
|
const parsed = ListEventsSchema.parse(args);
|
|
80
80
|
const client = clientFactory();
|
|
81
|
-
const result = await
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
]);
|
|
89
92
|
const events = result.items || [];
|
|
90
93
|
if (events.length === 0) {
|
|
91
94
|
return {
|
|
@@ -157,8 +160,8 @@ export function listEventsTool(server, clientFactory) {
|
|
|
157
160
|
: event.description;
|
|
158
161
|
output += `**Description:** ${truncated}\n`;
|
|
159
162
|
}
|
|
160
|
-
// Link —
|
|
161
|
-
const eventUrl = buildCalendarEventUrl(event.htmlLink);
|
|
163
|
+
// Link — `?eid=&authuser=<email>` form so multi-account readers don't 404.
|
|
164
|
+
const eventUrl = buildCalendarEventUrl(event.htmlLink, accountEmail);
|
|
162
165
|
if (eventUrl) {
|
|
163
166
|
output += `**Link:** ${eventUrl}\n`;
|
|
164
167
|
}
|
|
@@ -191,7 +191,10 @@ export function updateEventTool(server, clientFactory) {
|
|
|
191
191
|
if (hasAttachments) {
|
|
192
192
|
options.supportsAttachments = true;
|
|
193
193
|
}
|
|
194
|
-
const result = await
|
|
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
|
+
]);
|
|
195
198
|
let output = `# Event Updated Successfully\n\n`;
|
|
196
199
|
output += `## ${result.summary || '(No title)'}\n\n`;
|
|
197
200
|
output += `**Event ID:** ${result.id}\n`;
|
|
@@ -239,8 +242,8 @@ export function updateEventTool(server, clientFactory) {
|
|
|
239
242
|
output += ` - ${attachment.title || attachment.fileUrl}\n`;
|
|
240
243
|
}
|
|
241
244
|
}
|
|
242
|
-
// Link —
|
|
243
|
-
const eventUrl = buildCalendarEventUrl(result.htmlLink);
|
|
245
|
+
// Link — `?eid=&authuser=<email>` form so multi-account readers don't 404.
|
|
246
|
+
const eventUrl = buildCalendarEventUrl(result.htmlLink, accountEmail);
|
|
244
247
|
if (eventUrl) {
|
|
245
248
|
output += `\n**Event Link:** ${eventUrl}\n`;
|
|
246
249
|
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Builds a
|
|
2
|
+
* Builds a multi-account-robust Google Calendar event URL on `calendar.google.com`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Emits `https://calendar.google.com/calendar/event?eid=<eid>&authuser=<accountEmail>`
|
|
5
|
+
* when `accountEmail` is non-empty. The `authuser` query parameter forces Google
|
|
6
|
+
* Calendar to evaluate the link against the named account regardless of which
|
|
7
|
+
* Google account is the reader's browser default — without it, multi-account
|
|
8
|
+
* readers whose default account isn't the calendar owner get a 404.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Falls back to `https://calendar.google.com/calendar/event?eid=<eid>` (bare form)
|
|
11
|
+
* when `accountEmail` is empty/undefined. The bare form works for single-account
|
|
12
|
+
* readers, and the eid itself base64-decodes to `<event-id> <calendar-id>` so the
|
|
13
|
+
* calendar context still travels with the URL.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* NEVER emits the `/calendar/u/<email>/r/eventedit/<eid>` path form — that
|
|
16
|
+
* reader-side path-based account selector 404s for any reader not currently
|
|
17
|
+
* signed in as `<email>` (shipped and reverted as v0.0.9; see PR #3670 follow-up).
|
|
18
|
+
*
|
|
19
|
+
* Falls back to the original `htmlLink` if it cannot be parsed (e.g. the API
|
|
20
|
+
* response omitted `htmlLink` or it lacks an `eid` parameter), and returns
|
|
21
|
+
* `undefined` if `htmlLink` is missing entirely.
|
|
17
22
|
*/
|
|
18
|
-
export declare function buildCalendarEventUrl(htmlLink: string | undefined): string | undefined;
|
|
23
|
+
export declare function buildCalendarEventUrl(htmlLink: string | undefined, accountEmail?: string): string | undefined;
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Builds a
|
|
2
|
+
* Builds a multi-account-robust Google Calendar event URL on `calendar.google.com`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Emits `https://calendar.google.com/calendar/event?eid=<eid>&authuser=<accountEmail>`
|
|
5
|
+
* when `accountEmail` is non-empty. The `authuser` query parameter forces Google
|
|
6
|
+
* Calendar to evaluate the link against the named account regardless of which
|
|
7
|
+
* Google account is the reader's browser default — without it, multi-account
|
|
8
|
+
* readers whose default account isn't the calendar owner get a 404.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Falls back to `https://calendar.google.com/calendar/event?eid=<eid>` (bare form)
|
|
11
|
+
* when `accountEmail` is empty/undefined. The bare form works for single-account
|
|
12
|
+
* readers, and the eid itself base64-decodes to `<event-id> <calendar-id>` so the
|
|
13
|
+
* calendar context still travels with the URL.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* NEVER emits the `/calendar/u/<email>/r/eventedit/<eid>` path form — that
|
|
16
|
+
* reader-side path-based account selector 404s for any reader not currently
|
|
17
|
+
* signed in as `<email>` (shipped and reverted as v0.0.9; see PR #3670 follow-up).
|
|
18
|
+
*
|
|
19
|
+
* Falls back to the original `htmlLink` if it cannot be parsed (e.g. the API
|
|
20
|
+
* response omitted `htmlLink` or it lacks an `eid` parameter), and returns
|
|
21
|
+
* `undefined` if `htmlLink` is missing entirely.
|
|
17
22
|
*/
|
|
18
|
-
export function buildCalendarEventUrl(htmlLink) {
|
|
23
|
+
export function buildCalendarEventUrl(htmlLink, accountEmail) {
|
|
19
24
|
if (!htmlLink) {
|
|
20
25
|
return undefined;
|
|
21
26
|
}
|
|
@@ -23,7 +28,11 @@ export function buildCalendarEventUrl(htmlLink) {
|
|
|
23
28
|
if (!eid) {
|
|
24
29
|
return htmlLink;
|
|
25
30
|
}
|
|
26
|
-
|
|
31
|
+
const base = `https://calendar.google.com/calendar/event?eid=${eid}`;
|
|
32
|
+
if (accountEmail && accountEmail.length > 0) {
|
|
33
|
+
return `${base}&authuser=${encodeURIComponent(accountEmail)}`;
|
|
34
|
+
}
|
|
35
|
+
return base;
|
|
27
36
|
}
|
|
28
37
|
function extractEid(htmlLink) {
|
|
29
38
|
try {
|