@softeria/ms-365-mcp-server 0.1.11 → 0.2.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.
@@ -0,0 +1,814 @@
1
+ import { z } from 'zod';
2
+
3
+ export function registerCalendarTools(server, graphClient) {
4
+ server.tool('list-calendars', {}, async () => {
5
+ return graphClient.graphRequest('/me/calendars', {
6
+ method: 'GET',
7
+ });
8
+ });
9
+
10
+ server.tool('get-default-calendar', {}, async () => {
11
+ return graphClient.graphRequest('/me/calendar', {
12
+ method: 'GET',
13
+ });
14
+ });
15
+
16
+ server.tool(
17
+ 'list-events',
18
+ {
19
+ calendarId: z
20
+ .string()
21
+ .optional()
22
+ .describe('ID of the calendar (leave empty for default calendar)'),
23
+ startDateTime: z
24
+ .string()
25
+ .optional()
26
+ .describe('Start date/time in ISO format (e.g., "2023-01-01T00:00:00Z")'),
27
+ endDateTime: z
28
+ .string()
29
+ .optional()
30
+ .describe('End date/time in ISO format (e.g., "2023-12-31T23:59:59Z")'),
31
+ top: z.number().optional().default(10).describe('Maximum number of events to retrieve'),
32
+ filter: z.string().optional().describe('OData filter query (e.g., "subject eq \'Meeting\'")'),
33
+ orderBy: z
34
+ .string()
35
+ .optional()
36
+ .default('start/dateTime')
37
+ .describe('Order by clause (e.g., "start/dateTime asc")'),
38
+ },
39
+ async ({ calendarId, startDateTime, endDateTime, top, filter, orderBy }) => {
40
+ let endpoint = calendarId ? `/me/calendars/${calendarId}/events` : '/me/calendar/events';
41
+
42
+ const queryParams = [];
43
+
44
+ if (startDateTime && endDateTime) {
45
+ endpoint = endpoint.replace('/events', '/calendarView');
46
+ queryParams.push(`startDateTime=${encodeURIComponent(startDateTime)}`);
47
+ queryParams.push(`endDateTime=${encodeURIComponent(endDateTime)}`);
48
+ } else {
49
+ if (startDateTime || endDateTime) {
50
+ let dateFilter = '';
51
+ if (startDateTime) {
52
+ dateFilter += `start/dateTime ge '${startDateTime}'`;
53
+ }
54
+ if (endDateTime) {
55
+ if (dateFilter) dateFilter += ' and ';
56
+ dateFilter += `end/dateTime le '${endDateTime}'`;
57
+ }
58
+
59
+ if (filter) {
60
+ filter = `(${filter}) and (${dateFilter})`;
61
+ } else {
62
+ filter = dateFilter;
63
+ }
64
+ }
65
+ }
66
+
67
+ if (filter) {
68
+ queryParams.push(`$filter=${encodeURIComponent(filter)}`);
69
+ }
70
+
71
+ if (top) {
72
+ queryParams.push(`$top=${top}`);
73
+ }
74
+
75
+ if (orderBy) {
76
+ queryParams.push(`$orderBy=${encodeURIComponent(orderBy)}`);
77
+ }
78
+
79
+ if (queryParams.length > 0) {
80
+ endpoint += '?' + queryParams.join('&');
81
+ }
82
+
83
+ return graphClient.graphRequest(endpoint, {
84
+ method: 'GET',
85
+ });
86
+ }
87
+ );
88
+
89
+ server.tool(
90
+ 'get-event',
91
+ {
92
+ eventId: z.string().describe('ID of the event to retrieve'),
93
+ calendarId: z
94
+ .string()
95
+ .optional()
96
+ .describe('ID of the calendar (leave empty for default calendar)'),
97
+ },
98
+ async ({ eventId, calendarId }) => {
99
+ const endpoint = calendarId
100
+ ? `/me/calendars/${calendarId}/events/${eventId}`
101
+ : `/me/calendar/events/${eventId}`;
102
+
103
+ return graphClient.graphRequest(endpoint, {
104
+ method: 'GET',
105
+ });
106
+ }
107
+ );
108
+
109
+ server.tool(
110
+ 'create-event',
111
+ {
112
+ subject: z.string().describe('Event subject/title'),
113
+ body: z.string().optional().describe('Event description/body'),
114
+ startDateTime: z
115
+ .string()
116
+ .describe('Start date/time in ISO format (e.g., "2023-04-15T09:00:00")'),
117
+ endDateTime: z.string().describe('End date/time in ISO format (e.g., "2023-04-15T10:00:00")'),
118
+ timeZone: z.string().optional().default('UTC').describe('Time zone for the event'),
119
+ location: z.string().optional().describe('Event location'),
120
+ isAllDay: z.boolean().optional().default(false).describe('Whether this is an all-day event'),
121
+ attendees: z.array(z.string()).optional().describe('Email addresses of attendees'),
122
+ optionalAttendees: z
123
+ .array(z.string())
124
+ .optional()
125
+ .describe('Email addresses of optional attendees'),
126
+ reminderMinutesBeforeStart: z
127
+ .number()
128
+ .optional()
129
+ .describe('Reminder time in minutes before event starts'),
130
+ isOnlineMeeting: z
131
+ .boolean()
132
+ .optional()
133
+ .default(false)
134
+ .describe('Create an online meeting for this event'),
135
+ sensitivity: z
136
+ .enum(['normal', 'personal', 'private', 'confidential'])
137
+ .optional()
138
+ .default('normal')
139
+ .describe('Sensitivity level of the event'),
140
+ showAs: z
141
+ .enum(['free', 'tentative', 'busy', 'oof', 'workingElsewhere', 'unknown'])
142
+ .optional()
143
+ .default('busy')
144
+ .describe('How the event shows in calendar (free/busy status)'),
145
+ importance: z
146
+ .enum(['low', 'normal', 'high'])
147
+ .optional()
148
+ .default('normal')
149
+ .describe('Importance of the event'),
150
+ categories: z.array(z.string()).optional().describe('Categories/tags for the event'),
151
+ calendarId: z
152
+ .string()
153
+ .optional()
154
+ .describe('ID of the calendar (leave empty for default calendar)'),
155
+ },
156
+ async ({
157
+ subject,
158
+ body,
159
+ startDateTime,
160
+ endDateTime,
161
+ timeZone,
162
+ location,
163
+ isAllDay,
164
+ attendees,
165
+ optionalAttendees,
166
+ reminderMinutesBeforeStart,
167
+ isOnlineMeeting,
168
+ sensitivity,
169
+ showAs,
170
+ importance,
171
+ categories,
172
+ calendarId,
173
+ }) => {
174
+ const event = {
175
+ subject,
176
+ isAllDay,
177
+ start: {
178
+ dateTime: startDateTime,
179
+ timeZone: timeZone,
180
+ },
181
+ end: {
182
+ dateTime: endDateTime,
183
+ timeZone: timeZone,
184
+ },
185
+ sensitivity,
186
+ showAs,
187
+ importance,
188
+ };
189
+
190
+ if (body) {
191
+ event.body = {
192
+ contentType: 'html',
193
+ content: body,
194
+ };
195
+ }
196
+
197
+ if (location) {
198
+ event.location = {
199
+ displayName: location,
200
+ };
201
+ }
202
+
203
+ const allAttendees = [];
204
+
205
+ if (attendees && attendees.length > 0) {
206
+ allAttendees.push(
207
+ ...attendees.map((email) => ({
208
+ emailAddress: {
209
+ address: email,
210
+ },
211
+ type: 'required',
212
+ }))
213
+ );
214
+ }
215
+
216
+ if (optionalAttendees && optionalAttendees.length > 0) {
217
+ allAttendees.push(
218
+ ...optionalAttendees.map((email) => ({
219
+ emailAddress: {
220
+ address: email,
221
+ },
222
+ type: 'optional',
223
+ }))
224
+ );
225
+ }
226
+
227
+ if (allAttendees.length > 0) {
228
+ event.attendees = allAttendees;
229
+ }
230
+
231
+ if (reminderMinutesBeforeStart !== undefined) {
232
+ event.reminderMinutesBeforeStart = reminderMinutesBeforeStart;
233
+ event.isReminderOn = true;
234
+ }
235
+
236
+ if (isOnlineMeeting) {
237
+ event.isOnlineMeeting = true;
238
+ event.onlineMeetingProvider = 'teamsForBusiness';
239
+ }
240
+
241
+ if (categories && categories.length > 0) {
242
+ event.categories = categories;
243
+ }
244
+
245
+ const endpoint = calendarId ? `/me/calendars/${calendarId}/events` : '/me/calendar/events';
246
+
247
+ return graphClient.graphRequest(endpoint, {
248
+ method: 'POST',
249
+ body: JSON.stringify(event),
250
+ });
251
+ }
252
+ );
253
+
254
+ server.tool(
255
+ 'update-event',
256
+ {
257
+ eventId: z.string().describe('ID of the event to update'),
258
+ subject: z.string().optional().describe('Updated event subject/title'),
259
+ body: z.string().optional().describe('Updated event description/body'),
260
+ startDateTime: z.string().optional().describe('Updated start date/time in ISO format'),
261
+ endDateTime: z.string().optional().describe('Updated end date/time in ISO format'),
262
+ timeZone: z.string().optional().describe('Updated time zone for the event'),
263
+ location: z.string().optional().describe('Updated event location'),
264
+ isAllDay: z.boolean().optional().describe('Updated all-day flag'),
265
+ calendarId: z
266
+ .string()
267
+ .optional()
268
+ .describe('ID of the calendar (leave empty for default calendar)'),
269
+ },
270
+ async ({
271
+ eventId,
272
+ subject,
273
+ body,
274
+ startDateTime,
275
+ endDateTime,
276
+ timeZone,
277
+ location,
278
+ isAllDay,
279
+ calendarId,
280
+ }) => {
281
+ const eventUpdate = {};
282
+
283
+ if (subject !== undefined) {
284
+ eventUpdate.subject = subject;
285
+ }
286
+
287
+ if (body !== undefined) {
288
+ eventUpdate.body = {
289
+ contentType: 'html',
290
+ content: body,
291
+ };
292
+ }
293
+
294
+ if (startDateTime !== undefined) {
295
+ eventUpdate.start = {
296
+ dateTime: startDateTime,
297
+ timeZone: timeZone || 'UTC',
298
+ };
299
+ }
300
+
301
+ if (endDateTime !== undefined) {
302
+ eventUpdate.end = {
303
+ dateTime: endDateTime,
304
+ timeZone: timeZone || 'UTC',
305
+ };
306
+ }
307
+
308
+ if (location !== undefined) {
309
+ eventUpdate.location = {
310
+ displayName: location,
311
+ };
312
+ }
313
+
314
+ if (isAllDay !== undefined) {
315
+ eventUpdate.isAllDay = isAllDay;
316
+ }
317
+
318
+ const endpoint = calendarId
319
+ ? `/me/calendars/${calendarId}/events/${eventId}`
320
+ : `/me/calendar/events/${eventId}`;
321
+
322
+ return graphClient.graphRequest(endpoint, {
323
+ method: 'PATCH',
324
+ body: JSON.stringify(eventUpdate),
325
+ });
326
+ }
327
+ );
328
+
329
+ server.tool(
330
+ 'delete-event',
331
+ {
332
+ eventId: z.string().describe('ID of the event to delete'),
333
+ calendarId: z
334
+ .string()
335
+ .optional()
336
+ .describe('ID of the calendar (leave empty for default calendar)'),
337
+ },
338
+ async ({ eventId, calendarId }) => {
339
+ const endpoint = calendarId
340
+ ? `/me/calendars/${calendarId}/events/${eventId}`
341
+ : `/me/calendar/events/${eventId}`;
342
+
343
+ return graphClient.graphRequest(endpoint, {
344
+ method: 'DELETE',
345
+ });
346
+ }
347
+ );
348
+
349
+ server.tool(
350
+ 'accept-event',
351
+ {
352
+ eventId: z.string().describe('ID of the event to accept'),
353
+ comment: z.string().optional().describe('Optional comment with your response'),
354
+ sendResponse: z
355
+ .boolean()
356
+ .optional()
357
+ .default(true)
358
+ .describe('Send a response to the organizer'),
359
+ calendarId: z
360
+ .string()
361
+ .optional()
362
+ .describe('ID of the calendar (leave empty for default calendar)'),
363
+ },
364
+ async ({ eventId, comment, sendResponse, calendarId }) => {
365
+ const endpoint = calendarId
366
+ ? `/me/calendars/${calendarId}/events/${eventId}/accept`
367
+ : `/me/calendar/events/${eventId}/accept`;
368
+
369
+ const body = { sendResponse };
370
+ if (comment) {
371
+ body.comment = comment;
372
+ }
373
+
374
+ return graphClient.graphRequest(endpoint, {
375
+ method: 'POST',
376
+ body: JSON.stringify(body),
377
+ });
378
+ }
379
+ );
380
+
381
+ server.tool(
382
+ 'decline-event',
383
+ {
384
+ eventId: z.string().describe('ID of the event to decline'),
385
+ comment: z.string().optional().describe('Optional comment with your response'),
386
+ sendResponse: z
387
+ .boolean()
388
+ .optional()
389
+ .default(true)
390
+ .describe('Send a response to the organizer'),
391
+ calendarId: z
392
+ .string()
393
+ .optional()
394
+ .describe('ID of the calendar (leave empty for default calendar)'),
395
+ },
396
+ async ({ eventId, comment, sendResponse, calendarId }) => {
397
+ const endpoint = calendarId
398
+ ? `/me/calendars/${calendarId}/events/${eventId}/decline`
399
+ : `/me/calendar/events/${eventId}/decline`;
400
+
401
+ const body = { sendResponse };
402
+ if (comment) {
403
+ body.comment = comment;
404
+ }
405
+
406
+ return graphClient.graphRequest(endpoint, {
407
+ method: 'POST',
408
+ body: JSON.stringify(body),
409
+ });
410
+ }
411
+ );
412
+
413
+ server.tool(
414
+ 'find-meeting-times',
415
+ {
416
+ attendees: z.array(z.string()).describe('Email addresses of required attendees'),
417
+ durationMinutes: z.number().default(30).describe('Desired meeting duration in minutes'),
418
+ startDateTime: z.string().describe('Start date/time to search from, in ISO format'),
419
+ endDateTime: z.string().describe('End date/time to search until, in ISO format'),
420
+ timeZone: z.string().optional().default('UTC').describe('Time zone for the meeting'),
421
+ minimumAttendeePercentage: z
422
+ .number()
423
+ .optional()
424
+ .default(100)
425
+ .describe('Minimum percentage of attendees required'),
426
+ },
427
+ async ({
428
+ attendees,
429
+ durationMinutes,
430
+ startDateTime,
431
+ endDateTime,
432
+ timeZone,
433
+ minimumAttendeePercentage,
434
+ }) => {
435
+ const attendeesList = attendees.map((email) => ({
436
+ type: 'required',
437
+ emailAddress: {
438
+ address: email,
439
+ },
440
+ }));
441
+
442
+ const meetingTimeRequest = {
443
+ attendees: attendeesList,
444
+ timeConstraint: {
445
+ timeslots: [
446
+ {
447
+ start: {
448
+ dateTime: startDateTime,
449
+ timeZone,
450
+ },
451
+ end: {
452
+ dateTime: endDateTime,
453
+ timeZone,
454
+ },
455
+ },
456
+ ],
457
+ },
458
+ meetingDuration: `PT${durationMinutes}M`,
459
+ returnSuggestionReasons: true,
460
+ minimumAttendeePercentage,
461
+ };
462
+
463
+ return graphClient.graphRequest('/me/findMeetingTimes', {
464
+ method: 'POST',
465
+ body: JSON.stringify(meetingTimeRequest),
466
+ });
467
+ }
468
+ );
469
+
470
+ server.tool(
471
+ 'get-schedules',
472
+ {
473
+ schedules: z.array(z.string()).describe('Email addresses of users or resource rooms'),
474
+ startDateTime: z.string().describe('Start date/time in ISO format'),
475
+ endDateTime: z.string().describe('End date/time in ISO format'),
476
+ timeZone: z.string().optional().default('UTC').describe('Time zone for the schedule'),
477
+ },
478
+ async ({ schedules, startDateTime, endDateTime, timeZone }) => {
479
+ const scheduleRequest = {
480
+ schedules,
481
+ startTime: {
482
+ dateTime: startDateTime,
483
+ timeZone,
484
+ },
485
+ endTime: {
486
+ dateTime: endDateTime,
487
+ timeZone,
488
+ },
489
+ availabilityViewInterval: 30,
490
+ };
491
+
492
+ return graphClient.graphRequest('/me/calendar/getSchedule', {
493
+ method: 'POST',
494
+ body: JSON.stringify(scheduleRequest),
495
+ });
496
+ }
497
+ );
498
+
499
+ server.tool(
500
+ 'get-detailed-events',
501
+ {
502
+ calendarId: z
503
+ .string()
504
+ .optional()
505
+ .describe('ID of the calendar (leave empty for default calendar)'),
506
+ startDateTime: z.string().optional().describe('Start date/time in ISO format'),
507
+ endDateTime: z.string().optional().describe('End date/time in ISO format'),
508
+ includeAttendees: z
509
+ .boolean()
510
+ .optional()
511
+ .default(true)
512
+ .describe('Include attendee information'),
513
+ includeBody: z.boolean().optional().default(false).describe('Include event body content'),
514
+ includeExtensions: z.boolean().optional().default(false).describe('Include event extensions'),
515
+ includeInstances: z
516
+ .boolean()
517
+ .optional()
518
+ .default(false)
519
+ .describe('Include recurring event instances'),
520
+ },
521
+ async ({
522
+ calendarId,
523
+ startDateTime,
524
+ endDateTime,
525
+ includeAttendees,
526
+ includeBody,
527
+ includeExtensions,
528
+ includeInstances,
529
+ }) => {
530
+ let endpoint = calendarId ? `/me/calendars/${calendarId}/events` : '/me/calendar/events';
531
+
532
+ const queryParams = [];
533
+ const selectFields = [
534
+ 'id',
535
+ 'subject',
536
+ 'organizer',
537
+ 'start',
538
+ 'end',
539
+ 'location',
540
+ 'isOnlineMeeting',
541
+ 'onlineMeetingUrl',
542
+ ];
543
+ const expandFields = [];
544
+
545
+ if (startDateTime && endDateTime) {
546
+ endpoint = endpoint.replace('/events', '/calendarView');
547
+ queryParams.push(`startDateTime=${encodeURIComponent(startDateTime)}`);
548
+ queryParams.push(`endDateTime=${encodeURIComponent(endDateTime)}`);
549
+ }
550
+
551
+ if (includeAttendees) {
552
+ selectFields.push('attendees');
553
+ }
554
+
555
+ if (includeBody) {
556
+ selectFields.push('body');
557
+ }
558
+
559
+ if (includeExtensions) {
560
+ expandFields.push('extensions');
561
+ }
562
+
563
+ if (includeInstances) {
564
+ expandFields.push('instances');
565
+ }
566
+
567
+ queryParams.push(`$select=${selectFields.join(',')}`);
568
+
569
+ if (expandFields.length > 0) {
570
+ queryParams.push(`$expand=${expandFields.join(',')}`);
571
+ }
572
+
573
+ if (queryParams.length > 0) {
574
+ endpoint += '?' + queryParams.join('&');
575
+ }
576
+
577
+ return graphClient.graphRequest(endpoint, {
578
+ method: 'GET',
579
+ });
580
+ }
581
+ );
582
+
583
+ server.tool(
584
+ 'tentatively-accept-event',
585
+ {
586
+ eventId: z.string().describe('ID of the event to tentatively accept'),
587
+ comment: z.string().optional().describe('Optional comment with your response'),
588
+ sendResponse: z
589
+ .boolean()
590
+ .optional()
591
+ .default(true)
592
+ .describe('Send a response to the organizer'),
593
+ calendarId: z
594
+ .string()
595
+ .optional()
596
+ .describe('ID of the calendar (leave empty for default calendar)'),
597
+ },
598
+ async ({ eventId, comment, sendResponse, calendarId }) => {
599
+ const endpoint = calendarId
600
+ ? `/me/calendars/${calendarId}/events/${eventId}/tentativelyAccept`
601
+ : `/me/calendar/events/${eventId}/tentativelyAccept`;
602
+
603
+ const body = { sendResponse };
604
+ if (comment) {
605
+ body.comment = comment;
606
+ }
607
+
608
+ return graphClient.graphRequest(endpoint, {
609
+ method: 'POST',
610
+ body: JSON.stringify(body),
611
+ });
612
+ }
613
+ );
614
+
615
+ server.tool(
616
+ 'create-recurring-event',
617
+ {
618
+ subject: z.string().describe('Event subject/title'),
619
+ body: z.string().optional().describe('Event description/body'),
620
+ startDateTime: z.string().describe('First occurrence start date/time in ISO format'),
621
+ endDateTime: z.string().describe('First occurrence end date/time in ISO format'),
622
+ timeZone: z.string().optional().default('UTC').describe('Time zone for the event'),
623
+ location: z.string().optional().describe('Event location'),
624
+ isAllDay: z.boolean().optional().default(false).describe('Whether this is an all-day event'),
625
+ attendees: z.array(z.string()).optional().describe('Email addresses of required attendees'),
626
+ optionalAttendees: z
627
+ .array(z.string())
628
+ .optional()
629
+ .describe('Email addresses of optional attendees'),
630
+ recurrenceType: z
631
+ .enum(['daily', 'weekly', 'monthly', 'yearly'])
632
+ .describe('Type of recurrence'),
633
+ interval: z
634
+ .number()
635
+ .default(1)
636
+ .describe('Interval between occurrences (e.g., 2 for every 2 weeks)'),
637
+ daysOfWeek: z
638
+ .array(
639
+ z.enum(['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'])
640
+ )
641
+ .optional()
642
+ .describe('Days of the week for weekly recurrence'),
643
+ dayOfMonth: z.number().optional().describe('Day of the month for monthly recurrence'),
644
+ monthOfYear: z.number().optional().describe('Month of the year for yearly recurrence (1-12)'),
645
+ endAfterOccurrences: z.number().optional().describe('End after this many occurrences'),
646
+ endByDate: z.string().optional().describe('End by this date (ISO format)'),
647
+ reminderMinutesBeforeStart: z
648
+ .number()
649
+ .optional()
650
+ .describe('Reminder time in minutes before event starts'),
651
+ isOnlineMeeting: z
652
+ .boolean()
653
+ .optional()
654
+ .default(false)
655
+ .describe('Create an online meeting for this event'),
656
+ sensitivity: z
657
+ .enum(['normal', 'personal', 'private', 'confidential'])
658
+ .optional()
659
+ .default('normal')
660
+ .describe('Sensitivity level of the event'),
661
+ showAs: z
662
+ .enum(['free', 'tentative', 'busy', 'oof', 'workingElsewhere', 'unknown'])
663
+ .optional()
664
+ .default('busy')
665
+ .describe('How the event shows in calendar (free/busy status)'),
666
+ importance: z
667
+ .enum(['low', 'normal', 'high'])
668
+ .optional()
669
+ .default('normal')
670
+ .describe('Importance of the event'),
671
+ categories: z.array(z.string()).optional().describe('Categories/tags for the event'),
672
+ calendarId: z
673
+ .string()
674
+ .optional()
675
+ .describe('ID of the calendar (leave empty for default calendar)'),
676
+ },
677
+ async ({
678
+ subject,
679
+ body,
680
+ startDateTime,
681
+ endDateTime,
682
+ timeZone,
683
+ location,
684
+ isAllDay,
685
+ attendees,
686
+ optionalAttendees,
687
+ recurrenceType,
688
+ interval,
689
+ daysOfWeek,
690
+ dayOfMonth,
691
+ monthOfYear,
692
+ endAfterOccurrences,
693
+ endByDate,
694
+ reminderMinutesBeforeStart,
695
+ isOnlineMeeting,
696
+ sensitivity,
697
+ showAs,
698
+ importance,
699
+ categories,
700
+ calendarId,
701
+ }) => {
702
+ let pattern = {
703
+ type: recurrenceType,
704
+ interval: interval,
705
+ };
706
+
707
+ if (recurrenceType === 'weekly' && daysOfWeek && daysOfWeek.length > 0) {
708
+ pattern.daysOfWeek = daysOfWeek;
709
+ } else if (recurrenceType === 'monthly' && dayOfMonth) {
710
+ pattern.dayOfMonth = dayOfMonth;
711
+ } else if (recurrenceType === 'yearly' && monthOfYear) {
712
+ pattern.month = monthOfYear;
713
+ if (dayOfMonth) {
714
+ pattern.dayOfMonth = dayOfMonth;
715
+ }
716
+ }
717
+
718
+ let recurrenceRange = {
719
+ type: 'noEnd',
720
+ startDate: startDateTime.split('T')[0],
721
+ };
722
+
723
+ if (endAfterOccurrences) {
724
+ recurrenceRange.type = 'numbered';
725
+ recurrenceRange.numberOfOccurrences = endAfterOccurrences;
726
+ } else if (endByDate) {
727
+ recurrenceRange.type = 'endDate';
728
+ recurrenceRange.endDate = endByDate.split('T')[0];
729
+ }
730
+
731
+ const event = {
732
+ subject,
733
+ isAllDay,
734
+ start: {
735
+ dateTime: startDateTime,
736
+ timeZone: timeZone,
737
+ },
738
+ end: {
739
+ dateTime: endDateTime,
740
+ timeZone: timeZone,
741
+ },
742
+ recurrence: {
743
+ pattern: pattern,
744
+ range: recurrenceRange,
745
+ },
746
+ sensitivity,
747
+ showAs,
748
+ importance,
749
+ };
750
+
751
+ if (body) {
752
+ event.body = {
753
+ contentType: 'html',
754
+ content: body,
755
+ };
756
+ }
757
+
758
+ if (location) {
759
+ event.location = {
760
+ displayName: location,
761
+ };
762
+ }
763
+
764
+ const allAttendees = [];
765
+
766
+ if (attendees && attendees.length > 0) {
767
+ allAttendees.push(
768
+ ...attendees.map((email) => ({
769
+ emailAddress: {
770
+ address: email,
771
+ },
772
+ type: 'required',
773
+ }))
774
+ );
775
+ }
776
+
777
+ if (optionalAttendees && optionalAttendees.length > 0) {
778
+ allAttendees.push(
779
+ ...optionalAttendees.map((email) => ({
780
+ emailAddress: {
781
+ address: email,
782
+ },
783
+ type: 'optional',
784
+ }))
785
+ );
786
+ }
787
+
788
+ if (allAttendees.length > 0) {
789
+ event.attendees = allAttendees;
790
+ }
791
+
792
+ if (reminderMinutesBeforeStart !== undefined) {
793
+ event.reminderMinutesBeforeStart = reminderMinutesBeforeStart;
794
+ event.isReminderOn = true;
795
+ }
796
+
797
+ if (isOnlineMeeting) {
798
+ event.isOnlineMeeting = true;
799
+ event.onlineMeetingProvider = 'teamsForBusiness';
800
+ }
801
+
802
+ if (categories && categories.length > 0) {
803
+ event.categories = categories;
804
+ }
805
+
806
+ const endpoint = calendarId ? `/me/calendars/${calendarId}/events` : '/me/calendar/events';
807
+
808
+ return graphClient.graphRequest(endpoint, {
809
+ method: 'POST',
810
+ body: JSON.stringify(event),
811
+ });
812
+ }
813
+ );
814
+ }