@jnss95/ical-mcp 1.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/build/index.js ADDED
@@ -0,0 +1,707 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * iCal MCP Server - Main entry point
4
+ */
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import { z } from 'zod';
8
+ import { loadCalendar, saveCalendar, createCalendarFile } from './calendar/index.js';
9
+ // Store loaded calendars in memory
10
+ const calendars = new Map();
11
+ /**
12
+ * Create and configure the MCP server
13
+ */
14
+ export function createServer() {
15
+ const server = new McpServer({
16
+ name: 'ical-mcp',
17
+ version: '1.0.0',
18
+ });
19
+ // Register all tools
20
+ registerTools(server);
21
+ return server;
22
+ }
23
+ /**
24
+ * Register all calendar tools with the MCP server
25
+ */
26
+ function registerTools(server) {
27
+ // Tool: Load Calendar
28
+ server.tool('load_calendar', 'Load a calendar from a file path or URL. The calendar will be stored in memory and can be referenced by the provided ID.', {
29
+ calendarId: z.string().describe('A unique identifier for this calendar (used to reference it in other operations)'),
30
+ source: z.string().describe('File path or HTTP/HTTPS URL to the iCal file'),
31
+ }, async (args) => {
32
+ try {
33
+ const calendar = await loadCalendar(args.source);
34
+ calendars.set(args.calendarId, calendar);
35
+ const metadata = calendar.getMetadata();
36
+ return {
37
+ content: [
38
+ {
39
+ type: 'text',
40
+ text: JSON.stringify({
41
+ success: true,
42
+ calendarId: args.calendarId,
43
+ metadata,
44
+ message: `Calendar loaded successfully with ${metadata.eventCount} events`,
45
+ }, null, 2),
46
+ },
47
+ ],
48
+ };
49
+ }
50
+ catch (error) {
51
+ return {
52
+ content: [
53
+ {
54
+ type: 'text',
55
+ text: JSON.stringify({
56
+ success: false,
57
+ error: error instanceof Error ? error.message : 'Unknown error',
58
+ }, null, 2),
59
+ },
60
+ ],
61
+ isError: true,
62
+ };
63
+ }
64
+ });
65
+ // Tool: Create Calendar
66
+ server.tool('create_calendar', 'Create a new empty calendar file at the specified path.', {
67
+ calendarId: z.string().describe('A unique identifier for this calendar'),
68
+ filePath: z.string().describe('File path where the calendar will be created'),
69
+ name: z.string().optional().describe('Optional name for the calendar'),
70
+ timezone: z.string().optional().describe('Optional timezone (e.g., "America/New_York")'),
71
+ }, async (args) => {
72
+ try {
73
+ const calendar = await createCalendarFile(args.filePath, { name: args.name, timezone: args.timezone });
74
+ calendars.set(args.calendarId, calendar);
75
+ return {
76
+ content: [
77
+ {
78
+ type: 'text',
79
+ text: JSON.stringify({
80
+ success: true,
81
+ calendarId: args.calendarId,
82
+ filePath: args.filePath,
83
+ message: 'Calendar created successfully',
84
+ }, null, 2),
85
+ },
86
+ ],
87
+ };
88
+ }
89
+ catch (error) {
90
+ return {
91
+ content: [
92
+ {
93
+ type: 'text',
94
+ text: JSON.stringify({
95
+ success: false,
96
+ error: error instanceof Error ? error.message : 'Unknown error',
97
+ }, null, 2),
98
+ },
99
+ ],
100
+ isError: true,
101
+ };
102
+ }
103
+ });
104
+ // Tool: Get Calendar Info
105
+ server.tool('get_calendar_info', 'Get metadata and information about a loaded calendar.', {
106
+ calendarId: z.string().describe('The calendar identifier'),
107
+ }, async (args) => {
108
+ const calendar = calendars.get(args.calendarId);
109
+ if (!calendar) {
110
+ return {
111
+ content: [
112
+ {
113
+ type: 'text',
114
+ text: JSON.stringify({
115
+ success: false,
116
+ error: `Calendar "${args.calendarId}" not found. Load it first using load_calendar.`,
117
+ }, null, 2),
118
+ },
119
+ ],
120
+ isError: true,
121
+ };
122
+ }
123
+ const metadata = calendar.getMetadata();
124
+ const source = calendar.getSource();
125
+ return {
126
+ content: [
127
+ {
128
+ type: 'text',
129
+ text: JSON.stringify({
130
+ success: true,
131
+ calendarId: args.calendarId,
132
+ metadata,
133
+ source: {
134
+ type: source.type,
135
+ writable: source.writable,
136
+ },
137
+ }, null, 2),
138
+ },
139
+ ],
140
+ };
141
+ });
142
+ // Tool: List Events
143
+ server.tool('list_events', 'List all events in a calendar, optionally filtered by date range.', {
144
+ calendarId: z.string().describe('The calendar identifier'),
145
+ startDate: z.string().optional().describe('Start of date range (ISO 8601 format, e.g., "2024-01-01")'),
146
+ endDate: z.string().optional().describe('End of date range (ISO 8601 format, e.g., "2024-12-31")'),
147
+ limit: z.number().optional().describe('Maximum number of events to return'),
148
+ offset: z.number().optional().describe('Number of events to skip (for pagination)'),
149
+ expandRecurring: z.boolean().optional().describe('Expand recurring events into individual occurrences (default: true)'),
150
+ }, async (args) => {
151
+ const calendar = calendars.get(args.calendarId);
152
+ if (!calendar) {
153
+ return {
154
+ content: [
155
+ {
156
+ type: 'text',
157
+ text: JSON.stringify({
158
+ success: false,
159
+ error: `Calendar "${args.calendarId}" not found`,
160
+ }, null, 2),
161
+ },
162
+ ],
163
+ isError: true,
164
+ };
165
+ }
166
+ const options = {
167
+ startDate: args.startDate,
168
+ endDate: args.endDate,
169
+ limit: args.limit,
170
+ offset: args.offset,
171
+ expandRecurring: args.expandRecurring ?? true,
172
+ };
173
+ const result = calendar.queryEvents(options);
174
+ return {
175
+ content: [
176
+ {
177
+ type: 'text',
178
+ text: JSON.stringify({
179
+ success: true,
180
+ ...result,
181
+ }, null, 2),
182
+ },
183
+ ],
184
+ };
185
+ });
186
+ // Tool: Search Events
187
+ server.tool('search_events', 'Search for events matching specified criteria.', {
188
+ calendarId: z.string().describe('The calendar identifier'),
189
+ searchText: z.string().optional().describe('Text to search for in summary, description, and location'),
190
+ location: z.string().optional().describe('Filter by location'),
191
+ organizer: z.string().optional().describe('Filter by organizer'),
192
+ categories: z.array(z.string()).optional().describe('Filter by categories'),
193
+ startDate: z.string().optional().describe('Start of date range (ISO 8601)'),
194
+ endDate: z.string().optional().describe('End of date range (ISO 8601)'),
195
+ limit: z.number().optional().describe('Maximum number of results'),
196
+ }, async (args) => {
197
+ const calendar = calendars.get(args.calendarId);
198
+ if (!calendar) {
199
+ return {
200
+ content: [
201
+ {
202
+ type: 'text',
203
+ text: JSON.stringify({
204
+ success: false,
205
+ error: `Calendar "${args.calendarId}" not found`,
206
+ }, null, 2),
207
+ },
208
+ ],
209
+ isError: true,
210
+ };
211
+ }
212
+ const options = {
213
+ searchText: args.searchText,
214
+ location: args.location,
215
+ organizer: args.organizer,
216
+ categories: args.categories,
217
+ startDate: args.startDate,
218
+ endDate: args.endDate,
219
+ limit: args.limit,
220
+ expandRecurring: true,
221
+ };
222
+ const result = calendar.queryEvents(options);
223
+ return {
224
+ content: [
225
+ {
226
+ type: 'text',
227
+ text: JSON.stringify({
228
+ success: true,
229
+ ...result,
230
+ }, null, 2),
231
+ },
232
+ ],
233
+ };
234
+ });
235
+ // Tool: Get Event
236
+ server.tool('get_event', 'Get detailed information about a specific event by its UID.', {
237
+ calendarId: z.string().describe('The calendar identifier'),
238
+ eventId: z.string().describe('The event UID'),
239
+ }, async (args) => {
240
+ const calendar = calendars.get(args.calendarId);
241
+ if (!calendar) {
242
+ return {
243
+ content: [
244
+ {
245
+ type: 'text',
246
+ text: JSON.stringify({
247
+ success: false,
248
+ error: `Calendar "${args.calendarId}" not found`,
249
+ }, null, 2),
250
+ },
251
+ ],
252
+ isError: true,
253
+ };
254
+ }
255
+ const event = calendar.getEvent(args.eventId);
256
+ if (!event) {
257
+ return {
258
+ content: [
259
+ {
260
+ type: 'text',
261
+ text: JSON.stringify({
262
+ success: false,
263
+ error: `Event "${args.eventId}" not found`,
264
+ }, null, 2),
265
+ },
266
+ ],
267
+ isError: true,
268
+ };
269
+ }
270
+ return {
271
+ content: [
272
+ {
273
+ type: 'text',
274
+ text: JSON.stringify({
275
+ success: true,
276
+ event,
277
+ }, null, 2),
278
+ },
279
+ ],
280
+ };
281
+ });
282
+ // Tool: Create Event
283
+ server.tool('create_event', 'Create a new event in the calendar.', {
284
+ calendarId: z.string().describe('The calendar identifier'),
285
+ summary: z.string().describe('Event title/summary'),
286
+ start: z.string().describe('Start date/time (ISO 8601 format, e.g., "2024-01-15T10:00:00")'),
287
+ end: z.string().optional().describe('End date/time (ISO 8601 format). If not provided, defaults to start + 1 hour'),
288
+ description: z.string().optional().describe('Event description'),
289
+ location: z.string().optional().describe('Event location'),
290
+ allDay: z.boolean().optional().describe('Whether this is an all-day event'),
291
+ rrule: z.string().optional().describe('Recurrence rule (e.g., "FREQ=WEEKLY;COUNT=10")'),
292
+ organizer: z.string().optional().describe('Organizer email'),
293
+ attendees: z.array(z.string()).optional().describe('List of attendee emails'),
294
+ url: z.string().optional().describe('Event URL'),
295
+ categories: z.array(z.string()).optional().describe('Event categories'),
296
+ status: z.enum(['CONFIRMED', 'TENTATIVE', 'CANCELLED']).optional().describe('Event status'),
297
+ }, async (args) => {
298
+ const calendar = calendars.get(args.calendarId);
299
+ if (!calendar) {
300
+ return {
301
+ content: [
302
+ {
303
+ type: 'text',
304
+ text: JSON.stringify({
305
+ success: false,
306
+ error: `Calendar "${args.calendarId}" not found`,
307
+ }, null, 2),
308
+ },
309
+ ],
310
+ isError: true,
311
+ };
312
+ }
313
+ if (!calendar.isWritable()) {
314
+ return {
315
+ content: [
316
+ {
317
+ type: 'text',
318
+ text: JSON.stringify({
319
+ success: false,
320
+ error: 'Calendar is read-only',
321
+ }, null, 2),
322
+ },
323
+ ],
324
+ isError: true,
325
+ };
326
+ }
327
+ try {
328
+ const options = {
329
+ summary: args.summary,
330
+ start: args.start,
331
+ end: args.end,
332
+ description: args.description,
333
+ location: args.location,
334
+ allDay: args.allDay,
335
+ rrule: args.rrule,
336
+ organizer: args.organizer,
337
+ attendees: args.attendees,
338
+ url: args.url,
339
+ categories: args.categories,
340
+ status: args.status,
341
+ };
342
+ const event = calendar.createEvent(options);
343
+ // Save changes if this is a file-based calendar
344
+ const source = calendar.getSource();
345
+ if (source.type === 'file') {
346
+ await saveCalendar(calendar);
347
+ }
348
+ return {
349
+ content: [
350
+ {
351
+ type: 'text',
352
+ text: JSON.stringify({
353
+ success: true,
354
+ message: 'Event created successfully',
355
+ event,
356
+ }, null, 2),
357
+ },
358
+ ],
359
+ };
360
+ }
361
+ catch (error) {
362
+ return {
363
+ content: [
364
+ {
365
+ type: 'text',
366
+ text: JSON.stringify({
367
+ success: false,
368
+ error: error instanceof Error ? error.message : 'Unknown error',
369
+ }, null, 2),
370
+ },
371
+ ],
372
+ isError: true,
373
+ };
374
+ }
375
+ });
376
+ // Tool: Update Event
377
+ server.tool('update_event', 'Update an existing event.', {
378
+ calendarId: z.string().describe('The calendar identifier'),
379
+ eventId: z.string().describe('The event UID to update'),
380
+ summary: z.string().optional().describe('New event title/summary'),
381
+ start: z.string().optional().describe('New start date/time (ISO 8601 format)'),
382
+ end: z.string().optional().describe('New end date/time (ISO 8601 format)'),
383
+ description: z.string().optional().describe('New event description'),
384
+ location: z.string().optional().describe('New event location'),
385
+ allDay: z.boolean().optional().describe('Whether this is an all-day event'),
386
+ rrule: z.string().optional().describe('New recurrence rule'),
387
+ organizer: z.string().optional().describe('New organizer email'),
388
+ attendees: z.array(z.string()).optional().describe('New list of attendee emails'),
389
+ url: z.string().optional().describe('New event URL'),
390
+ categories: z.array(z.string()).optional().describe('New event categories'),
391
+ status: z.enum(['CONFIRMED', 'TENTATIVE', 'CANCELLED']).optional().describe('New event status'),
392
+ }, async (args) => {
393
+ const calendar = calendars.get(args.calendarId);
394
+ if (!calendar) {
395
+ return {
396
+ content: [
397
+ {
398
+ type: 'text',
399
+ text: JSON.stringify({
400
+ success: false,
401
+ error: `Calendar "${args.calendarId}" not found`,
402
+ }, null, 2),
403
+ },
404
+ ],
405
+ isError: true,
406
+ };
407
+ }
408
+ if (!calendar.isWritable()) {
409
+ return {
410
+ content: [
411
+ {
412
+ type: 'text',
413
+ text: JSON.stringify({
414
+ success: false,
415
+ error: 'Calendar is read-only',
416
+ }, null, 2),
417
+ },
418
+ ],
419
+ isError: true,
420
+ };
421
+ }
422
+ try {
423
+ const options = {
424
+ summary: args.summary,
425
+ start: args.start,
426
+ end: args.end,
427
+ description: args.description,
428
+ location: args.location,
429
+ allDay: args.allDay,
430
+ rrule: args.rrule,
431
+ organizer: args.organizer,
432
+ attendees: args.attendees,
433
+ url: args.url,
434
+ categories: args.categories,
435
+ status: args.status,
436
+ };
437
+ const event = calendar.updateEvent(args.eventId, options);
438
+ // Save changes if this is a file-based calendar
439
+ const source = calendar.getSource();
440
+ if (source.type === 'file') {
441
+ await saveCalendar(calendar);
442
+ }
443
+ return {
444
+ content: [
445
+ {
446
+ type: 'text',
447
+ text: JSON.stringify({
448
+ success: true,
449
+ message: 'Event updated successfully',
450
+ event,
451
+ }, null, 2),
452
+ },
453
+ ],
454
+ };
455
+ }
456
+ catch (error) {
457
+ return {
458
+ content: [
459
+ {
460
+ type: 'text',
461
+ text: JSON.stringify({
462
+ success: false,
463
+ error: error instanceof Error ? error.message : 'Unknown error',
464
+ }, null, 2),
465
+ },
466
+ ],
467
+ isError: true,
468
+ };
469
+ }
470
+ });
471
+ // Tool: Delete Event
472
+ server.tool('delete_event', 'Delete an event from the calendar.', {
473
+ calendarId: z.string().describe('The calendar identifier'),
474
+ eventId: z.string().describe('The event UID to delete'),
475
+ }, async (args) => {
476
+ const calendar = calendars.get(args.calendarId);
477
+ if (!calendar) {
478
+ return {
479
+ content: [
480
+ {
481
+ type: 'text',
482
+ text: JSON.stringify({
483
+ success: false,
484
+ error: `Calendar "${args.calendarId}" not found`,
485
+ }, null, 2),
486
+ },
487
+ ],
488
+ isError: true,
489
+ };
490
+ }
491
+ if (!calendar.isWritable()) {
492
+ return {
493
+ content: [
494
+ {
495
+ type: 'text',
496
+ text: JSON.stringify({
497
+ success: false,
498
+ error: 'Calendar is read-only',
499
+ }, null, 2),
500
+ },
501
+ ],
502
+ isError: true,
503
+ };
504
+ }
505
+ try {
506
+ const deleted = calendar.deleteEvent(args.eventId);
507
+ if (!deleted) {
508
+ return {
509
+ content: [
510
+ {
511
+ type: 'text',
512
+ text: JSON.stringify({
513
+ success: false,
514
+ error: `Event "${args.eventId}" not found`,
515
+ }, null, 2),
516
+ },
517
+ ],
518
+ isError: true,
519
+ };
520
+ }
521
+ // Save changes if this is a file-based calendar
522
+ const source = calendar.getSource();
523
+ if (source.type === 'file') {
524
+ await saveCalendar(calendar);
525
+ }
526
+ return {
527
+ content: [
528
+ {
529
+ type: 'text',
530
+ text: JSON.stringify({
531
+ success: true,
532
+ message: 'Event deleted successfully',
533
+ eventId: args.eventId,
534
+ }, null, 2),
535
+ },
536
+ ],
537
+ };
538
+ }
539
+ catch (error) {
540
+ return {
541
+ content: [
542
+ {
543
+ type: 'text',
544
+ text: JSON.stringify({
545
+ success: false,
546
+ error: error instanceof Error ? error.message : 'Unknown error',
547
+ }, null, 2),
548
+ },
549
+ ],
550
+ isError: true,
551
+ };
552
+ }
553
+ });
554
+ // Tool: List Loaded Calendars
555
+ server.tool('list_loaded_calendars', 'List all currently loaded calendars.', {}, async () => {
556
+ const calendarList = Array.from(calendars.entries()).map(([id, calendar]) => ({
557
+ id,
558
+ metadata: calendar.getMetadata(),
559
+ source: {
560
+ type: calendar.getSource().type,
561
+ writable: calendar.getSource().writable,
562
+ },
563
+ }));
564
+ return {
565
+ content: [
566
+ {
567
+ type: 'text',
568
+ text: JSON.stringify({
569
+ success: true,
570
+ calendars: calendarList,
571
+ count: calendarList.length,
572
+ }, null, 2),
573
+ },
574
+ ],
575
+ };
576
+ });
577
+ // Tool: Unload Calendar
578
+ server.tool('unload_calendar', 'Unload a calendar from memory.', {
579
+ calendarId: z.string().describe('The calendar identifier to unload'),
580
+ }, async (args) => {
581
+ if (!calendars.has(args.calendarId)) {
582
+ return {
583
+ content: [
584
+ {
585
+ type: 'text',
586
+ text: JSON.stringify({
587
+ success: false,
588
+ error: `Calendar "${args.calendarId}" not found`,
589
+ }, null, 2),
590
+ },
591
+ ],
592
+ isError: true,
593
+ };
594
+ }
595
+ calendars.delete(args.calendarId);
596
+ return {
597
+ content: [
598
+ {
599
+ type: 'text',
600
+ text: JSON.stringify({
601
+ success: true,
602
+ message: `Calendar "${args.calendarId}" unloaded successfully`,
603
+ }, null, 2),
604
+ },
605
+ ],
606
+ };
607
+ });
608
+ // Tool: Export Calendar
609
+ server.tool('export_calendar', 'Export a calendar as iCal format string.', {
610
+ calendarId: z.string().describe('The calendar identifier'),
611
+ }, async (args) => {
612
+ const calendar = calendars.get(args.calendarId);
613
+ if (!calendar) {
614
+ return {
615
+ content: [
616
+ {
617
+ type: 'text',
618
+ text: JSON.stringify({
619
+ success: false,
620
+ error: `Calendar "${args.calendarId}" not found`,
621
+ }, null, 2),
622
+ },
623
+ ],
624
+ isError: true,
625
+ };
626
+ }
627
+ return {
628
+ content: [
629
+ {
630
+ type: 'text',
631
+ text: calendar.toString(),
632
+ },
633
+ ],
634
+ };
635
+ });
636
+ }
637
+ /**
638
+ * Load calendars from environment variables
639
+ * Supports ICAL_URL, ICAL_FILE, or ICAL_CALENDARS (JSON array)
640
+ */
641
+ async function loadCalendarsFromEnv() {
642
+ // Single calendar from URL
643
+ const icalUrl = process.env.ICAL_URL;
644
+ if (icalUrl) {
645
+ try {
646
+ const calendar = await loadCalendar(icalUrl);
647
+ const alias = process.env.ICAL_ALIAS || 'default';
648
+ calendars.set(alias, calendar);
649
+ console.error(`Loaded calendar "${alias}" from ${icalUrl}`);
650
+ }
651
+ catch (error) {
652
+ console.error(`Failed to load calendar from ICAL_URL: ${error}`);
653
+ }
654
+ }
655
+ // Single calendar from file
656
+ const icalFile = process.env.ICAL_FILE;
657
+ if (icalFile) {
658
+ try {
659
+ const calendar = await loadCalendar(icalFile);
660
+ const alias = process.env.ICAL_ALIAS || 'default';
661
+ calendars.set(alias, calendar);
662
+ console.error(`Loaded calendar "${alias}" from ${icalFile}`);
663
+ }
664
+ catch (error) {
665
+ console.error(`Failed to load calendar from ICAL_FILE: ${error}`);
666
+ }
667
+ }
668
+ // Multiple calendars from JSON
669
+ const icalCalendars = process.env.ICAL_CALENDARS;
670
+ if (icalCalendars) {
671
+ try {
672
+ const configs = JSON.parse(icalCalendars);
673
+ for (const config of configs) {
674
+ try {
675
+ const calendar = await loadCalendar(config.source);
676
+ calendars.set(config.alias, calendar);
677
+ console.error(`Loaded calendar "${config.alias}" from ${config.source}`);
678
+ }
679
+ catch (error) {
680
+ console.error(`Failed to load calendar "${config.alias}": ${error}`);
681
+ }
682
+ }
683
+ }
684
+ catch (error) {
685
+ console.error(`Failed to parse ICAL_CALENDARS: ${error}`);
686
+ }
687
+ }
688
+ }
689
+ /**
690
+ * Main function to run the MCP server
691
+ */
692
+ async function main() {
693
+ // Load any calendars configured via environment variables
694
+ await loadCalendarsFromEnv();
695
+ const server = createServer();
696
+ const transport = new StdioServerTransport();
697
+ await server.connect(transport);
698
+ console.error('iCal MCP Server running on stdio');
699
+ }
700
+ // Run the server
701
+ main().catch((error) => {
702
+ console.error('Fatal error in main():', error);
703
+ process.exit(1);
704
+ });
705
+ // Export for testing
706
+ export { calendars };
707
+ //# sourceMappingURL=index.js.map