google-tools-mcp 1.2.11 → 2.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/LICENSE +21 -21
- package/README.md +555 -276
- package/dist/cachedToolsList.js +52 -52
- package/dist/googleDocsApiHelpers.js +76 -12
- package/dist/helpers.js +572 -252
- package/dist/httpAuth.js +296 -0
- package/dist/index.js +162 -11
- package/dist/logger.js +87 -87
- package/dist/markdown-transformer/docsToMarkdown.js +222 -21
- package/dist/markdown-transformer/index.js +19 -9
- package/dist/markdown-transformer/markdownToDocs.js +458 -28
- package/dist/readTracker.js +136 -17
- package/dist/sessionContext.js +37 -0
- package/dist/setup.js +187 -9
- package/dist/tools/calendar/getBusy.js +64 -64
- package/dist/tools/calendar/getEvents.js +141 -141
- package/dist/tools/calendar/getFree.js +225 -225
- package/dist/tools/calendar/index.js +19 -19
- package/dist/tools/calendar/listCalendars.js +38 -38
- package/dist/tools/calendar/listRecurringInstances.js +83 -83
- package/dist/tools/calendar/manageCalendar.js +121 -121
- package/dist/tools/calendar/manageEvent.js +258 -258
- package/dist/tools/calendar/moveEvent.js +66 -66
- package/dist/tools/docs/addTab.js +10 -12
- package/dist/tools/docs/appendToGoogleDoc.js +9 -4
- package/dist/tools/docs/deleteRange.js +9 -4
- package/dist/tools/docs/findAndReplace.js +9 -4
- package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -1
- package/dist/tools/docs/formatting/getFormatting.js +171 -171
- package/dist/tools/docs/insertImage.js +15 -2
- package/dist/tools/docs/insertPageBreak.js +4 -1
- package/dist/tools/docs/insertTable.js +4 -1
- package/dist/tools/docs/insertTableWithData.js +4 -1
- package/dist/tools/docs/modifyText.js +14 -4
- package/dist/tools/docs/modifyText.test.js +84 -84
- package/dist/tools/docs/readGoogleDoc.js +71 -11
- package/dist/tools/docs/renameTab.js +12 -14
- package/dist/tools/drafts.js +165 -165
- package/dist/tools/drive/createDocument.js +13 -1
- package/dist/tools/drive/downloadFile.js +268 -268
- package/dist/tools/drive/getFileInfo.js +48 -48
- package/dist/tools/drive/getFilePath.js +50 -50
- package/dist/tools/drive/listDriveFiles.js +112 -112
- package/dist/tools/drive/listSharedDrives.js +52 -52
- package/dist/tools/drive/listSharedWithMe.js +97 -97
- package/dist/tools/drive/uploadFile.js +111 -111
- package/dist/tools/extras/index.js +9 -9
- package/dist/tools/extras/readDriveFile.js +212 -206
- package/dist/tools/extras/readFile.js +84 -84
- package/dist/tools/extras/searchFileContents.js +81 -81
- package/dist/tools/forms/batchUpdateForm.js +81 -81
- package/dist/tools/forms/createForm.js +54 -54
- package/dist/tools/forms/getForm.js +118 -118
- package/dist/tools/forms/getFormResponse.js +45 -45
- package/dist/tools/forms/index.js +15 -15
- package/dist/tools/forms/listFormResponses.js +56 -56
- package/dist/tools/forms/setPublishSettings.js +59 -59
- package/dist/tools/gmail/drafts.js +165 -165
- package/dist/tools/gmail/labels.js +54 -83
- package/dist/tools/gmail/messages.js +444 -441
- package/dist/tools/gmail/settings.js +358 -528
- package/dist/tools/gmail/threads.js +286 -164
- package/dist/tools/index.js +511 -473
- package/dist/tools/labels.js +103 -103
- package/dist/tools/legacyAliases.js +426 -0
- package/dist/tools/maps/directions.js +72 -0
- package/dist/tools/maps/geocode.js +16 -0
- package/dist/tools/maps/index.js +15 -0
- package/dist/tools/maps/mapsClient.js +83 -0
- package/dist/tools/maps/placeDetails.js +16 -0
- package/dist/tools/maps/reverseGeocode.js +19 -0
- package/dist/tools/maps/searchNearby.js +54 -0
- package/dist/tools/maps/searchPlaces.js +24 -0
- package/dist/tools/messages.js +448 -448
- package/dist/tools/settings.js +528 -528
- package/dist/tools/slides/createPresentation.js +104 -104
- package/dist/tools/slides/createShape.js +92 -92
- package/dist/tools/slides/createTextBox.js +82 -82
- package/dist/tools/slides/deleteSlide.js +30 -30
- package/dist/tools/slides/duplicateSlide.js +37 -37
- package/dist/tools/slides/exportThumbnail.js +42 -42
- package/dist/tools/slides/formatParagraph.js +72 -72
- package/dist/tools/slides/formatText.js +84 -84
- package/dist/tools/slides/getPresentation.js +87 -87
- package/dist/tools/slides/index.js +33 -33
- package/dist/tools/slides/reorderSlides.js +41 -41
- package/dist/tools/slides/replaceAllText.js +46 -46
- package/dist/tools/slides/setBackground.js +58 -58
- package/dist/tools/slides/speakerNotes.js +102 -102
- package/dist/tools/slides/styleShape.js +111 -111
- package/dist/tools/slides/updatePresentation.js +128 -128
- package/dist/tools/threads.js +145 -145
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +31 -7
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +72 -9
- package/dist/updateCheck.js +202 -0
- package/dist/workspace.js +122 -0
- package/package.json +82 -80
|
@@ -1,225 +1,225 @@
|
|
|
1
|
-
import { UserError } from 'fastmcp';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { getCalendarClient } from '../../clients.js';
|
|
4
|
-
|
|
5
|
-
export function register(server) {
|
|
6
|
-
server.addTool({
|
|
7
|
-
name: '
|
|
8
|
-
description:
|
|
9
|
-
'Finds available time slots where all specified people are free. ' +
|
|
10
|
-
'Takes a duration and working hours as boundaries, then returns open slots. ' +
|
|
11
|
-
'Uses the freebusy API under the hood — works across your org without calendar sharing.',
|
|
12
|
-
parameters: z.object({
|
|
13
|
-
time_min: z
|
|
14
|
-
.string()
|
|
15
|
-
.describe('Start of search range in RFC3339 format (e.g. "2025-03-15T00:00:00Z").'),
|
|
16
|
-
time_max: z
|
|
17
|
-
.string()
|
|
18
|
-
.describe('End of search range in RFC3339 format (e.g. "2025-03-21T23:59:59Z").'),
|
|
19
|
-
duration_minutes: z
|
|
20
|
-
.number()
|
|
21
|
-
.describe('Required slot duration in minutes (e.g. 30, 60).'),
|
|
22
|
-
calendar_ids: z
|
|
23
|
-
.array(z.string())
|
|
24
|
-
.optional()
|
|
25
|
-
.default(['primary'])
|
|
26
|
-
.describe('Calendar IDs or email addresses to check mutual availability. Defaults to ["primary"].'),
|
|
27
|
-
working_hours_start: z
|
|
28
|
-
.string()
|
|
29
|
-
.optional()
|
|
30
|
-
.default('09:00')
|
|
31
|
-
.describe('Start of working hours in HH:MM format (e.g. "09:00"). Defaults to "09:00".'),
|
|
32
|
-
working_hours_end: z
|
|
33
|
-
.string()
|
|
34
|
-
.optional()
|
|
35
|
-
.default('17:00')
|
|
36
|
-
.describe('End of working hours in HH:MM format (e.g. "17:00"). Defaults to "17:00".'),
|
|
37
|
-
timezone: z
|
|
38
|
-
.string()
|
|
39
|
-
.optional()
|
|
40
|
-
.default('UTC')
|
|
41
|
-
.describe('Timezone for working hours (e.g. "America/New_York"). Defaults to "UTC".'),
|
|
42
|
-
max_results: z
|
|
43
|
-
.number()
|
|
44
|
-
.optional()
|
|
45
|
-
.default(10)
|
|
46
|
-
.describe('Maximum number of available slots to return. Defaults to 10.'),
|
|
47
|
-
}),
|
|
48
|
-
execute: async (args, { log }) => {
|
|
49
|
-
const calendar = await getCalendarClient();
|
|
50
|
-
log.info(
|
|
51
|
-
`Finding ${args.duration_minutes}min free slots for ${args.calendar_ids.join(', ')} ` +
|
|
52
|
-
`between ${args.working_hours_start}-${args.working_hours_end} ${args.timezone}`
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
// 1. Query freebusy for all calendars
|
|
57
|
-
const response = await calendar.freebusy.query({
|
|
58
|
-
requestBody: {
|
|
59
|
-
timeMin: args.time_min,
|
|
60
|
-
timeMax: args.time_max,
|
|
61
|
-
timeZone: args.timezone,
|
|
62
|
-
items: args.calendar_ids.map((id) => ({ id })),
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const calendars = response.data.calendars || {};
|
|
67
|
-
|
|
68
|
-
// Check for errors
|
|
69
|
-
const errors = [];
|
|
70
|
-
for (const [calId, data] of Object.entries(calendars)) {
|
|
71
|
-
if (data.errors?.length) {
|
|
72
|
-
errors.push(`${calId}: ${data.errors[0].reason}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (errors.length) {
|
|
76
|
-
throw new UserError(
|
|
77
|
-
`Could not check availability for some calendars:\n${errors.join('\n')}`
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 2. Merge all busy blocks
|
|
82
|
-
const allBusy = [];
|
|
83
|
-
for (const data of Object.values(calendars)) {
|
|
84
|
-
for (const block of data.busy || []) {
|
|
85
|
-
allBusy.push({
|
|
86
|
-
start: new Date(block.start).getTime(),
|
|
87
|
-
end: new Date(block.end).getTime(),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Sort and merge overlapping busy blocks
|
|
93
|
-
allBusy.sort((a, b) => a.start - b.start);
|
|
94
|
-
const merged = [];
|
|
95
|
-
for (const block of allBusy) {
|
|
96
|
-
if (merged.length && block.start <= merged[merged.length - 1].end) {
|
|
97
|
-
merged[merged.length - 1].end = Math.max(merged[merged.length - 1].end, block.end);
|
|
98
|
-
} else {
|
|
99
|
-
merged.push({ ...block });
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 3. Find free slots within working hours
|
|
104
|
-
const rangeStart = new Date(args.time_min);
|
|
105
|
-
const rangeEnd = new Date(args.time_max);
|
|
106
|
-
const durationMs = args.duration_minutes * 60 * 1000;
|
|
107
|
-
const [whStartH, whStartM] = args.working_hours_start.split(':').map(Number);
|
|
108
|
-
const [whEndH, whEndM] = args.working_hours_end.split(':').map(Number);
|
|
109
|
-
|
|
110
|
-
const freeSlots = [];
|
|
111
|
-
const currentDay = new Date(rangeStart);
|
|
112
|
-
|
|
113
|
-
// Iterate day by day
|
|
114
|
-
while (currentDay < rangeEnd && freeSlots.length < args.max_results) {
|
|
115
|
-
const dayStart = getTimeInTimezone(currentDay, whStartH, whStartM, args.timezone);
|
|
116
|
-
const dayEnd = getTimeInTimezone(currentDay, whEndH, whEndM, args.timezone);
|
|
117
|
-
|
|
118
|
-
if (dayEnd > rangeStart.getTime() && dayStart < rangeEnd.getTime()) {
|
|
119
|
-
const effectiveStart = Math.max(dayStart, rangeStart.getTime());
|
|
120
|
-
const effectiveEnd = Math.min(dayEnd, rangeEnd.getTime());
|
|
121
|
-
|
|
122
|
-
// Find free windows in this day
|
|
123
|
-
const dayBusy = merged.filter(
|
|
124
|
-
(b) => b.start < effectiveEnd && b.end > effectiveStart
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
let cursor = effectiveStart;
|
|
128
|
-
for (const block of dayBusy) {
|
|
129
|
-
if (block.start > cursor) {
|
|
130
|
-
// Free gap before this busy block
|
|
131
|
-
const gapEnd = Math.min(block.start, effectiveEnd);
|
|
132
|
-
if (gapEnd - cursor >= durationMs && freeSlots.length < args.max_results) {
|
|
133
|
-
freeSlots.push({
|
|
134
|
-
start: new Date(cursor).toISOString(),
|
|
135
|
-
end: new Date(gapEnd).toISOString(),
|
|
136
|
-
duration_minutes: Math.round((gapEnd - cursor) / 60000),
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
cursor = Math.max(cursor, block.end);
|
|
141
|
-
}
|
|
142
|
-
// Free gap after last busy block
|
|
143
|
-
if (effectiveEnd - cursor >= durationMs && freeSlots.length < args.max_results) {
|
|
144
|
-
freeSlots.push({
|
|
145
|
-
start: new Date(cursor).toISOString(),
|
|
146
|
-
end: new Date(effectiveEnd).toISOString(),
|
|
147
|
-
duration_minutes: Math.round((effectiveEnd - cursor) / 60000),
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Move to next day
|
|
153
|
-
currentDay.setDate(currentDay.getDate() + 1);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return JSON.stringify(
|
|
157
|
-
{
|
|
158
|
-
query: {
|
|
159
|
-
calendars: args.calendar_ids,
|
|
160
|
-
duration_minutes: args.duration_minutes,
|
|
161
|
-
working_hours: `${args.working_hours_start}-${args.working_hours_end} ${args.timezone}`,
|
|
162
|
-
},
|
|
163
|
-
available_slots: freeSlots,
|
|
164
|
-
total_found: freeSlots.length,
|
|
165
|
-
},
|
|
166
|
-
null,
|
|
167
|
-
2
|
|
168
|
-
);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
if (error instanceof UserError) throw error;
|
|
171
|
-
log.error(`Error finding free slots: ${error.message || error}`);
|
|
172
|
-
throw new UserError(`Failed to find available slots: ${error.message || 'Unknown error'}`);
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Get a timestamp for a specific time-of-day on a given date in a timezone.
|
|
180
|
-
* Uses Intl.DateTimeFormat to resolve the timezone offset.
|
|
181
|
-
*/
|
|
182
|
-
function getTimeInTimezone(date, hours, minutes, timezone) {
|
|
183
|
-
// Build a date string for the target day
|
|
184
|
-
const year = date.getFullYear();
|
|
185
|
-
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
186
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
187
|
-
const hh = String(hours).padStart(2, '0');
|
|
188
|
-
const mm = String(minutes).padStart(2, '0');
|
|
189
|
-
|
|
190
|
-
// Create a date in the local timezone of the server, then adjust
|
|
191
|
-
// We use a formatter to figure out the offset
|
|
192
|
-
try {
|
|
193
|
-
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
194
|
-
timeZone: timezone,
|
|
195
|
-
year: 'numeric',
|
|
196
|
-
month: '2-digit',
|
|
197
|
-
day: '2-digit',
|
|
198
|
-
hour: '2-digit',
|
|
199
|
-
minute: '2-digit',
|
|
200
|
-
second: '2-digit',
|
|
201
|
-
hour12: false,
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// Create a reference point at midnight UTC for this date
|
|
205
|
-
const refUtc = new Date(`${year}-${month}-${day}T${hh}:${mm}:00Z`);
|
|
206
|
-
|
|
207
|
-
// Format that UTC time in the target timezone to see what time it appears as
|
|
208
|
-
const parts = formatter.formatToParts(refUtc);
|
|
209
|
-
const tzParts = {};
|
|
210
|
-
for (const p of parts) {
|
|
211
|
-
if (p.type !== 'literal') tzParts[p.type] = parseInt(p.value, 10);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Calculate the offset: what we wanted vs what we got
|
|
215
|
-
const gotMinutes = tzParts.hour * 60 + tzParts.minute;
|
|
216
|
-
const wantedMinutes = hours * 60 + minutes;
|
|
217
|
-
const diffMs = (gotMinutes - wantedMinutes) * 60 * 1000;
|
|
218
|
-
|
|
219
|
-
// Adjust: if the timezone shows a later time, the UTC equivalent is earlier
|
|
220
|
-
return refUtc.getTime() - diffMs;
|
|
221
|
-
} catch {
|
|
222
|
-
// Fallback: treat as UTC
|
|
223
|
-
return new Date(`${year}-${month}-${day}T${hh}:${mm}:00Z`).getTime();
|
|
224
|
-
}
|
|
225
|
-
}
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getCalendarClient } from '../../clients.js';
|
|
4
|
+
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'getFree',
|
|
8
|
+
description:
|
|
9
|
+
'Finds available time slots where all specified people are free. ' +
|
|
10
|
+
'Takes a duration and working hours as boundaries, then returns open slots. ' +
|
|
11
|
+
'Uses the freebusy API under the hood — works across your org without calendar sharing.',
|
|
12
|
+
parameters: z.object({
|
|
13
|
+
time_min: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe('Start of search range in RFC3339 format (e.g. "2025-03-15T00:00:00Z").'),
|
|
16
|
+
time_max: z
|
|
17
|
+
.string()
|
|
18
|
+
.describe('End of search range in RFC3339 format (e.g. "2025-03-21T23:59:59Z").'),
|
|
19
|
+
duration_minutes: z
|
|
20
|
+
.number()
|
|
21
|
+
.describe('Required slot duration in minutes (e.g. 30, 60).'),
|
|
22
|
+
calendar_ids: z
|
|
23
|
+
.array(z.string())
|
|
24
|
+
.optional()
|
|
25
|
+
.default(['primary'])
|
|
26
|
+
.describe('Calendar IDs or email addresses to check mutual availability. Defaults to ["primary"].'),
|
|
27
|
+
working_hours_start: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.default('09:00')
|
|
31
|
+
.describe('Start of working hours in HH:MM format (e.g. "09:00"). Defaults to "09:00".'),
|
|
32
|
+
working_hours_end: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.default('17:00')
|
|
36
|
+
.describe('End of working hours in HH:MM format (e.g. "17:00"). Defaults to "17:00".'),
|
|
37
|
+
timezone: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.default('UTC')
|
|
41
|
+
.describe('Timezone for working hours (e.g. "America/New_York"). Defaults to "UTC".'),
|
|
42
|
+
max_results: z
|
|
43
|
+
.number()
|
|
44
|
+
.optional()
|
|
45
|
+
.default(10)
|
|
46
|
+
.describe('Maximum number of available slots to return. Defaults to 10.'),
|
|
47
|
+
}),
|
|
48
|
+
execute: async (args, { log }) => {
|
|
49
|
+
const calendar = await getCalendarClient();
|
|
50
|
+
log.info(
|
|
51
|
+
`Finding ${args.duration_minutes}min free slots for ${args.calendar_ids.join(', ')} ` +
|
|
52
|
+
`between ${args.working_hours_start}-${args.working_hours_end} ${args.timezone}`
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// 1. Query freebusy for all calendars
|
|
57
|
+
const response = await calendar.freebusy.query({
|
|
58
|
+
requestBody: {
|
|
59
|
+
timeMin: args.time_min,
|
|
60
|
+
timeMax: args.time_max,
|
|
61
|
+
timeZone: args.timezone,
|
|
62
|
+
items: args.calendar_ids.map((id) => ({ id })),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const calendars = response.data.calendars || {};
|
|
67
|
+
|
|
68
|
+
// Check for errors
|
|
69
|
+
const errors = [];
|
|
70
|
+
for (const [calId, data] of Object.entries(calendars)) {
|
|
71
|
+
if (data.errors?.length) {
|
|
72
|
+
errors.push(`${calId}: ${data.errors[0].reason}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (errors.length) {
|
|
76
|
+
throw new UserError(
|
|
77
|
+
`Could not check availability for some calendars:\n${errors.join('\n')}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2. Merge all busy blocks
|
|
82
|
+
const allBusy = [];
|
|
83
|
+
for (const data of Object.values(calendars)) {
|
|
84
|
+
for (const block of data.busy || []) {
|
|
85
|
+
allBusy.push({
|
|
86
|
+
start: new Date(block.start).getTime(),
|
|
87
|
+
end: new Date(block.end).getTime(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Sort and merge overlapping busy blocks
|
|
93
|
+
allBusy.sort((a, b) => a.start - b.start);
|
|
94
|
+
const merged = [];
|
|
95
|
+
for (const block of allBusy) {
|
|
96
|
+
if (merged.length && block.start <= merged[merged.length - 1].end) {
|
|
97
|
+
merged[merged.length - 1].end = Math.max(merged[merged.length - 1].end, block.end);
|
|
98
|
+
} else {
|
|
99
|
+
merged.push({ ...block });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3. Find free slots within working hours
|
|
104
|
+
const rangeStart = new Date(args.time_min);
|
|
105
|
+
const rangeEnd = new Date(args.time_max);
|
|
106
|
+
const durationMs = args.duration_minutes * 60 * 1000;
|
|
107
|
+
const [whStartH, whStartM] = args.working_hours_start.split(':').map(Number);
|
|
108
|
+
const [whEndH, whEndM] = args.working_hours_end.split(':').map(Number);
|
|
109
|
+
|
|
110
|
+
const freeSlots = [];
|
|
111
|
+
const currentDay = new Date(rangeStart);
|
|
112
|
+
|
|
113
|
+
// Iterate day by day
|
|
114
|
+
while (currentDay < rangeEnd && freeSlots.length < args.max_results) {
|
|
115
|
+
const dayStart = getTimeInTimezone(currentDay, whStartH, whStartM, args.timezone);
|
|
116
|
+
const dayEnd = getTimeInTimezone(currentDay, whEndH, whEndM, args.timezone);
|
|
117
|
+
|
|
118
|
+
if (dayEnd > rangeStart.getTime() && dayStart < rangeEnd.getTime()) {
|
|
119
|
+
const effectiveStart = Math.max(dayStart, rangeStart.getTime());
|
|
120
|
+
const effectiveEnd = Math.min(dayEnd, rangeEnd.getTime());
|
|
121
|
+
|
|
122
|
+
// Find free windows in this day
|
|
123
|
+
const dayBusy = merged.filter(
|
|
124
|
+
(b) => b.start < effectiveEnd && b.end > effectiveStart
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
let cursor = effectiveStart;
|
|
128
|
+
for (const block of dayBusy) {
|
|
129
|
+
if (block.start > cursor) {
|
|
130
|
+
// Free gap before this busy block
|
|
131
|
+
const gapEnd = Math.min(block.start, effectiveEnd);
|
|
132
|
+
if (gapEnd - cursor >= durationMs && freeSlots.length < args.max_results) {
|
|
133
|
+
freeSlots.push({
|
|
134
|
+
start: new Date(cursor).toISOString(),
|
|
135
|
+
end: new Date(gapEnd).toISOString(),
|
|
136
|
+
duration_minutes: Math.round((gapEnd - cursor) / 60000),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
cursor = Math.max(cursor, block.end);
|
|
141
|
+
}
|
|
142
|
+
// Free gap after last busy block
|
|
143
|
+
if (effectiveEnd - cursor >= durationMs && freeSlots.length < args.max_results) {
|
|
144
|
+
freeSlots.push({
|
|
145
|
+
start: new Date(cursor).toISOString(),
|
|
146
|
+
end: new Date(effectiveEnd).toISOString(),
|
|
147
|
+
duration_minutes: Math.round((effectiveEnd - cursor) / 60000),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Move to next day
|
|
153
|
+
currentDay.setDate(currentDay.getDate() + 1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return JSON.stringify(
|
|
157
|
+
{
|
|
158
|
+
query: {
|
|
159
|
+
calendars: args.calendar_ids,
|
|
160
|
+
duration_minutes: args.duration_minutes,
|
|
161
|
+
working_hours: `${args.working_hours_start}-${args.working_hours_end} ${args.timezone}`,
|
|
162
|
+
},
|
|
163
|
+
available_slots: freeSlots,
|
|
164
|
+
total_found: freeSlots.length,
|
|
165
|
+
},
|
|
166
|
+
null,
|
|
167
|
+
2
|
|
168
|
+
);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof UserError) throw error;
|
|
171
|
+
log.error(`Error finding free slots: ${error.message || error}`);
|
|
172
|
+
throw new UserError(`Failed to find available slots: ${error.message || 'Unknown error'}`);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get a timestamp for a specific time-of-day on a given date in a timezone.
|
|
180
|
+
* Uses Intl.DateTimeFormat to resolve the timezone offset.
|
|
181
|
+
*/
|
|
182
|
+
function getTimeInTimezone(date, hours, minutes, timezone) {
|
|
183
|
+
// Build a date string for the target day
|
|
184
|
+
const year = date.getFullYear();
|
|
185
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
186
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
187
|
+
const hh = String(hours).padStart(2, '0');
|
|
188
|
+
const mm = String(minutes).padStart(2, '0');
|
|
189
|
+
|
|
190
|
+
// Create a date in the local timezone of the server, then adjust
|
|
191
|
+
// We use a formatter to figure out the offset
|
|
192
|
+
try {
|
|
193
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
194
|
+
timeZone: timezone,
|
|
195
|
+
year: 'numeric',
|
|
196
|
+
month: '2-digit',
|
|
197
|
+
day: '2-digit',
|
|
198
|
+
hour: '2-digit',
|
|
199
|
+
minute: '2-digit',
|
|
200
|
+
second: '2-digit',
|
|
201
|
+
hour12: false,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Create a reference point at midnight UTC for this date
|
|
205
|
+
const refUtc = new Date(`${year}-${month}-${day}T${hh}:${mm}:00Z`);
|
|
206
|
+
|
|
207
|
+
// Format that UTC time in the target timezone to see what time it appears as
|
|
208
|
+
const parts = formatter.formatToParts(refUtc);
|
|
209
|
+
const tzParts = {};
|
|
210
|
+
for (const p of parts) {
|
|
211
|
+
if (p.type !== 'literal') tzParts[p.type] = parseInt(p.value, 10);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Calculate the offset: what we wanted vs what we got
|
|
215
|
+
const gotMinutes = tzParts.hour * 60 + tzParts.minute;
|
|
216
|
+
const wantedMinutes = hours * 60 + minutes;
|
|
217
|
+
const diffMs = (gotMinutes - wantedMinutes) * 60 * 1000;
|
|
218
|
+
|
|
219
|
+
// Adjust: if the timezone shows a later time, the UTC equivalent is earlier
|
|
220
|
+
return refUtc.getTime() - diffMs;
|
|
221
|
+
} catch {
|
|
222
|
+
// Fallback: treat as UTC
|
|
223
|
+
return new Date(`${year}-${month}-${day}T${hh}:${mm}:00Z`).getTime();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { register as listCalendars } from './listCalendars.js';
|
|
2
|
-
import { register as getEvents } from './getEvents.js';
|
|
3
|
-
import { register as manageEvent } from './manageEvent.js';
|
|
4
|
-
import { register as getBusy } from './getBusy.js';
|
|
5
|
-
import { register as getFree } from './getFree.js';
|
|
6
|
-
import { register as moveEvent } from './moveEvent.js';
|
|
7
|
-
import { register as listRecurringInstances } from './listRecurringInstances.js';
|
|
8
|
-
import { register as manageCalendar } from './manageCalendar.js';
|
|
9
|
-
|
|
10
|
-
export function registerCalendarTools(server) {
|
|
11
|
-
listCalendars(server);
|
|
12
|
-
getEvents(server);
|
|
13
|
-
manageEvent(server);
|
|
14
|
-
getBusy(server);
|
|
15
|
-
getFree(server);
|
|
16
|
-
moveEvent(server);
|
|
17
|
-
listRecurringInstances(server);
|
|
18
|
-
manageCalendar(server);
|
|
19
|
-
}
|
|
1
|
+
import { register as listCalendars } from './listCalendars.js';
|
|
2
|
+
import { register as getEvents } from './getEvents.js';
|
|
3
|
+
import { register as manageEvent } from './manageEvent.js';
|
|
4
|
+
import { register as getBusy } from './getBusy.js';
|
|
5
|
+
import { register as getFree } from './getFree.js';
|
|
6
|
+
import { register as moveEvent } from './moveEvent.js';
|
|
7
|
+
import { register as listRecurringInstances } from './listRecurringInstances.js';
|
|
8
|
+
import { register as manageCalendar } from './manageCalendar.js';
|
|
9
|
+
|
|
10
|
+
export function registerCalendarTools(server) {
|
|
11
|
+
listCalendars(server);
|
|
12
|
+
getEvents(server);
|
|
13
|
+
manageEvent(server);
|
|
14
|
+
getBusy(server);
|
|
15
|
+
getFree(server);
|
|
16
|
+
moveEvent(server);
|
|
17
|
+
listRecurringInstances(server);
|
|
18
|
+
manageCalendar(server);
|
|
19
|
+
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import { UserError } from 'fastmcp';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { getCalendarClient } from '../../clients.js';
|
|
4
|
-
|
|
5
|
-
export function register(server) {
|
|
6
|
-
server.addTool({
|
|
7
|
-
name: '
|
|
8
|
-
description:
|
|
9
|
-
'Lists all calendars accessible to the authenticated user. Returns calendar name, ID, access role, and whether it is the primary calendar.',
|
|
10
|
-
parameters: z.object({}),
|
|
11
|
-
execute: async (_args, { log }) => {
|
|
12
|
-
const calendar = await getCalendarClient();
|
|
13
|
-
log.info('Listing calendars');
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const response = await calendar.calendarList.list();
|
|
17
|
-
const calendars = response.data.items || [];
|
|
18
|
-
|
|
19
|
-
const results = calendars.map((cal) => ({
|
|
20
|
-
id: cal.id,
|
|
21
|
-
summary: cal.summary || cal.id,
|
|
22
|
-
description: cal.description || null,
|
|
23
|
-
primary: cal.primary || false,
|
|
24
|
-
accessRole: cal.accessRole,
|
|
25
|
-
timeZone: cal.timeZone,
|
|
26
|
-
backgroundColor: cal.backgroundColor || null,
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
return JSON.stringify(results, null, 2);
|
|
30
|
-
} catch (error) {
|
|
31
|
-
log.error(`Error listing calendars: ${error.message || error}`);
|
|
32
|
-
if (error.code === 401)
|
|
33
|
-
throw new UserError('Authentication failed. Try logging out and re-authenticating.');
|
|
34
|
-
throw new UserError(`Failed to list calendars: ${error.message || 'Unknown error'}`);
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
}
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getCalendarClient } from '../../clients.js';
|
|
4
|
+
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'listCalendars',
|
|
8
|
+
description:
|
|
9
|
+
'Lists all calendars accessible to the authenticated user. Returns calendar name, ID, access role, and whether it is the primary calendar.',
|
|
10
|
+
parameters: z.object({}),
|
|
11
|
+
execute: async (_args, { log }) => {
|
|
12
|
+
const calendar = await getCalendarClient();
|
|
13
|
+
log.info('Listing calendars');
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const response = await calendar.calendarList.list();
|
|
17
|
+
const calendars = response.data.items || [];
|
|
18
|
+
|
|
19
|
+
const results = calendars.map((cal) => ({
|
|
20
|
+
id: cal.id,
|
|
21
|
+
summary: cal.summary || cal.id,
|
|
22
|
+
description: cal.description || null,
|
|
23
|
+
primary: cal.primary || false,
|
|
24
|
+
accessRole: cal.accessRole,
|
|
25
|
+
timeZone: cal.timeZone,
|
|
26
|
+
backgroundColor: cal.backgroundColor || null,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
return JSON.stringify(results, null, 2);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
log.error(`Error listing calendars: ${error.message || error}`);
|
|
32
|
+
if (error.code === 401)
|
|
33
|
+
throw new UserError('Authentication failed. Try logging out and re-authenticating.');
|
|
34
|
+
throw new UserError(`Failed to list calendars: ${error.message || 'Unknown error'}`);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|