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.
- package/build/index.integration-with-mock.js +8 -5
- package/package.json +2 -2
- package/shared/server.d.ts +8 -0
- package/shared/server.js +3 -0
- package/shared/tools/create-event.js +9 -4
- package/shared/tools/get-event.js +9 -4
- package/shared/tools/list-events.js +16 -11
- package/shared/tools/update-event.js +9 -4
- package/shared/utils/calendar-helpers.d.ts +18 -0
- package/shared/utils/calendar-helpers.js +36 -0
|
@@ -33,7 +33,7 @@ class MockCalendarClient {
|
|
|
33
33
|
timeZone: 'America/New_York',
|
|
34
34
|
},
|
|
35
35
|
status: 'confirmed',
|
|
36
|
-
htmlLink: 'https://
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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.
|
|
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.
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
33
|
"google-auth-library": "^10.5.0",
|
|
34
34
|
"zod": "^3.24.1"
|
|
35
35
|
},
|
package/shared/server.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
219
|
-
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
|
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
|
-
|
|
243
|
-
|
|
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
|
+
}
|