@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/README.md +250 -0
- package/build/calendar/calendar.d.ts +69 -0
- package/build/calendar/calendar.d.ts.map +1 -0
- package/build/calendar/calendar.js +455 -0
- package/build/calendar/calendar.js.map +1 -0
- package/build/calendar/index.d.ts +3 -0
- package/build/calendar/index.d.ts.map +1 -0
- package/build/calendar/index.js +3 -0
- package/build/calendar/index.js.map +1 -0
- package/build/calendar/loader.d.ts +32 -0
- package/build/calendar/loader.d.ts.map +1 -0
- package/build/calendar/loader.js +179 -0
- package/build/calendar/loader.js.map +1 -0
- package/build/index.d.ts +13 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +707 -0
- package/build/index.js.map +1 -0
- package/build/types/calendar.d.ts +176 -0
- package/build/types/calendar.d.ts.map +1 -0
- package/build/types/calendar.js +5 -0
- package/build/types/calendar.js.map +1 -0
- package/build/types/index.d.ts +2 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +2 -0
- package/build/types/index.js.map +1 -0
- package/package.json +62 -0
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
|