mcp-icloud 2.1.1
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/DISCLOSURE +35 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/auth/index.js +175 -0
- package/calendar/caldav-client.js +320 -0
- package/calendar/index.js +252 -0
- package/calendar/local-client.js +223 -0
- package/config.js +69 -0
- package/contacts/carddav-client.js +331 -0
- package/contacts/index.js +350 -0
- package/contacts/local-client.js +372 -0
- package/email/imap-client.js +351 -0
- package/email/index.js +278 -0
- package/email/local-client.js +257 -0
- package/email/smtp-client.js +79 -0
- package/index.js +123 -0
- package/messages/index.js +157 -0
- package/messages/local-client.js +74 -0
- package/mode.js +82 -0
- package/notes/index.js +162 -0
- package/notes/local-client.js +203 -0
- package/package.json +76 -0
- package/reminders/index.js +215 -0
- package/reminders/local-client.js +269 -0
- package/safari/index.js +122 -0
- package/safari/local-client.js +156 -0
- package/utils/applescript.js +214 -0
- package/utils/date-utils.js +72 -0
- package/utils/error-handler.js +71 -0
- package/utils/schemas.js +33 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CalDAV client for iCloud Calendar
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { DAVClient } = require('tsdav');
|
|
6
|
+
const ICAL = require('ical.js');
|
|
7
|
+
const config = require('../config');
|
|
8
|
+
const { getCredentials } = require('../auth');
|
|
9
|
+
|
|
10
|
+
let cachedClient = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get or create CalDAV client
|
|
14
|
+
*/
|
|
15
|
+
async function getClient() {
|
|
16
|
+
if (cachedClient) {
|
|
17
|
+
return cachedClient;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const creds = getCredentials();
|
|
21
|
+
|
|
22
|
+
const client = new DAVClient({
|
|
23
|
+
serverUrl: config.CALDAV.SERVER_URL,
|
|
24
|
+
credentials: {
|
|
25
|
+
username: creds.email,
|
|
26
|
+
password: creds.password
|
|
27
|
+
},
|
|
28
|
+
authMethod: 'Basic',
|
|
29
|
+
defaultAccountType: 'caldav'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await client.login();
|
|
34
|
+
cachedClient = client;
|
|
35
|
+
return client;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.message?.includes('401') || error.message?.includes('auth')) {
|
|
38
|
+
throw new Error('UNAUTHORIZED');
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Clear cached client (for re-auth)
|
|
46
|
+
*/
|
|
47
|
+
function clearClient() {
|
|
48
|
+
cachedClient = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get all calendars
|
|
53
|
+
*/
|
|
54
|
+
async function getCalendars() {
|
|
55
|
+
const client = await getClient();
|
|
56
|
+
const calendars = await client.fetchCalendars();
|
|
57
|
+
return calendars.map(cal => ({
|
|
58
|
+
url: cal.url,
|
|
59
|
+
displayName: cal.displayName || 'Unnamed Calendar',
|
|
60
|
+
ctag: cal.ctag,
|
|
61
|
+
syncToken: cal.syncToken
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse iCalendar event to simple object
|
|
67
|
+
*/
|
|
68
|
+
function parseEvent(icalData, url) {
|
|
69
|
+
try {
|
|
70
|
+
const jcalData = ICAL.parse(icalData);
|
|
71
|
+
const comp = new ICAL.Component(jcalData);
|
|
72
|
+
const vevent = comp.getFirstSubcomponent('vevent');
|
|
73
|
+
|
|
74
|
+
if (!vevent) return null;
|
|
75
|
+
|
|
76
|
+
const event = new ICAL.Event(vevent);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
url,
|
|
80
|
+
uid: event.uid,
|
|
81
|
+
summary: event.summary || '(No title)',
|
|
82
|
+
description: event.description || '',
|
|
83
|
+
location: event.location || '',
|
|
84
|
+
start: event.startDate?.toJSDate(),
|
|
85
|
+
end: event.endDate?.toJSDate(),
|
|
86
|
+
isAllDay: event.startDate?.isDate || false,
|
|
87
|
+
organizer: vevent.getFirstPropertyValue('organizer'),
|
|
88
|
+
attendees: vevent.getAllProperties('attendee').map(a => a.getFirstValue()),
|
|
89
|
+
status: event.status,
|
|
90
|
+
created: vevent.getFirstPropertyValue('created')?.toJSDate(),
|
|
91
|
+
lastModified: vevent.getFirstPropertyValue('last-modified')?.toJSDate()
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Error parsing event:', error.message);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* List events from all calendars
|
|
101
|
+
*/
|
|
102
|
+
async function listEvents(count = 25, daysAhead = 30) {
|
|
103
|
+
const client = await getClient();
|
|
104
|
+
const calendars = await client.fetchCalendars();
|
|
105
|
+
|
|
106
|
+
const now = new Date();
|
|
107
|
+
const endDate = new Date();
|
|
108
|
+
endDate.setDate(endDate.getDate() + daysAhead);
|
|
109
|
+
|
|
110
|
+
const allEvents = [];
|
|
111
|
+
|
|
112
|
+
for (const calendar of calendars) {
|
|
113
|
+
try {
|
|
114
|
+
const calendarObjects = await client.fetchCalendarObjects({
|
|
115
|
+
calendar,
|
|
116
|
+
timeRange: {
|
|
117
|
+
start: now.toISOString(),
|
|
118
|
+
end: endDate.toISOString()
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
for (const obj of calendarObjects) {
|
|
123
|
+
const event = parseEvent(obj.data, obj.url);
|
|
124
|
+
if (event) {
|
|
125
|
+
event.calendarName = calendar.displayName || 'Calendar';
|
|
126
|
+
allEvents.push(event);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`Error fetching from calendar ${calendar.displayName}:`, error.message);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Sort by start date
|
|
135
|
+
allEvents.sort((a, b) => (a.start || 0) - (b.start || 0));
|
|
136
|
+
|
|
137
|
+
return allEvents.slice(0, count);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create a new event
|
|
142
|
+
*/
|
|
143
|
+
async function createEvent({ summary, start, end, description, location, calendarUrl }) {
|
|
144
|
+
const client = await getClient();
|
|
145
|
+
|
|
146
|
+
// Get calendars if URL not provided
|
|
147
|
+
let targetCalendar;
|
|
148
|
+
if (calendarUrl) {
|
|
149
|
+
const calendars = await client.fetchCalendars();
|
|
150
|
+
targetCalendar = calendars.find(c => c.url === calendarUrl);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!targetCalendar) {
|
|
154
|
+
const calendars = await client.fetchCalendars();
|
|
155
|
+
targetCalendar = calendars[0]; // Use first calendar
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!targetCalendar) {
|
|
159
|
+
throw new Error('No calendar found');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Create iCalendar data
|
|
163
|
+
const uid = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}@icloud-mcp`;
|
|
164
|
+
|
|
165
|
+
const startDate = new Date(start);
|
|
166
|
+
const endDate = new Date(end);
|
|
167
|
+
|
|
168
|
+
const icalData = `BEGIN:VCALENDAR
|
|
169
|
+
VERSION:2.0
|
|
170
|
+
PRODID:-//iCloud MCP//EN
|
|
171
|
+
BEGIN:VEVENT
|
|
172
|
+
UID:${uid}
|
|
173
|
+
DTSTAMP:${formatICalDate(new Date())}
|
|
174
|
+
DTSTART:${formatICalDate(startDate)}
|
|
175
|
+
DTEND:${formatICalDate(endDate)}
|
|
176
|
+
SUMMARY:${escapeICalText(summary)}${description ? `\nDESCRIPTION:${escapeICalText(description)}` : ''}${location ? `\nLOCATION:${escapeICalText(location)}` : ''}
|
|
177
|
+
END:VEVENT
|
|
178
|
+
END:VCALENDAR`;
|
|
179
|
+
|
|
180
|
+
const result = await client.createCalendarObject({
|
|
181
|
+
calendar: targetCalendar,
|
|
182
|
+
filename: `${uid}.ics`,
|
|
183
|
+
iCalString: icalData
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
success: true,
|
|
188
|
+
uid,
|
|
189
|
+
url: result?.url,
|
|
190
|
+
calendar: targetCalendar.displayName
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Rewrite individual properties of an existing VEVENT.
|
|
196
|
+
*
|
|
197
|
+
* Deliberately a line-level edit rather than rebuilding the iCalendar from
|
|
198
|
+
* scratch: a rebuild would silently drop RRULE, ATTENDEE, VALARM and any other
|
|
199
|
+
* property this server does not model, which on a real calendar means losing
|
|
200
|
+
* recurrence and invitees on every edit.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} ical - Existing iCalendar text
|
|
203
|
+
* @param {Object} changes - Properties to set; undefined values are left alone
|
|
204
|
+
* @returns {string} - Updated iCalendar text
|
|
205
|
+
*/
|
|
206
|
+
function applyICalChanges(ical, { summary, start, end, description, location }) {
|
|
207
|
+
const updates = [];
|
|
208
|
+
if (summary !== undefined) updates.push(['SUMMARY', escapeICalText(summary)]);
|
|
209
|
+
if (start !== undefined) updates.push(['DTSTART', formatICalDate(new Date(start))]);
|
|
210
|
+
if (end !== undefined) updates.push(['DTEND', formatICalDate(new Date(end))]);
|
|
211
|
+
if (description !== undefined) updates.push(['DESCRIPTION', escapeICalText(description)]);
|
|
212
|
+
if (location !== undefined) updates.push(['LOCATION', escapeICalText(location)]);
|
|
213
|
+
updates.push(['DTSTAMP', formatICalDate(new Date())]);
|
|
214
|
+
|
|
215
|
+
const lines = ical.split(/\r?\n/);
|
|
216
|
+
|
|
217
|
+
for (const [key, value] of updates) {
|
|
218
|
+
// A property may carry parameters, e.g. DTSTART;TZID=Europe/Madrid:...
|
|
219
|
+
const idx = lines.findIndex(l => {
|
|
220
|
+
const upper = l.toUpperCase();
|
|
221
|
+
return upper.startsWith(key + ':') || upper.startsWith(key + ';');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (idx !== -1) {
|
|
225
|
+
lines[idx] = `${key}:${value}`;
|
|
226
|
+
} else {
|
|
227
|
+
const endIdx = lines.findIndex(l => l.toUpperCase().startsWith('END:VEVENT'));
|
|
228
|
+
lines.splice(endIdx === -1 ? lines.length : endIdx, 0, `${key}:${value}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return lines.join('\r\n');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Update an existing event, preserving every property not being changed.
|
|
237
|
+
*/
|
|
238
|
+
async function updateEvent(eventUrl, changes) {
|
|
239
|
+
const client = await getClient();
|
|
240
|
+
const calendars = await client.fetchCalendars();
|
|
241
|
+
|
|
242
|
+
let existing = null;
|
|
243
|
+
for (const calendar of calendars) {
|
|
244
|
+
try {
|
|
245
|
+
const objects = await client.fetchCalendarObjects({
|
|
246
|
+
calendar,
|
|
247
|
+
objectUrls: [eventUrl]
|
|
248
|
+
});
|
|
249
|
+
if (objects && objects.length && objects[0].data) {
|
|
250
|
+
existing = objects[0];
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
// Wrong calendar for this URL; keep looking.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!existing) {
|
|
259
|
+
throw new Error(`Event not found: ${eventUrl}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const updated = applyICalChanges(existing.data, changes);
|
|
263
|
+
|
|
264
|
+
await client.updateCalendarObject({
|
|
265
|
+
calendarObject: {
|
|
266
|
+
url: eventUrl,
|
|
267
|
+
data: updated,
|
|
268
|
+
etag: existing.etag || ''
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return { success: true, url: eventUrl };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Delete an event
|
|
277
|
+
*/
|
|
278
|
+
async function deleteEvent(eventUrl) {
|
|
279
|
+
const client = await getClient();
|
|
280
|
+
|
|
281
|
+
await client.deleteCalendarObject({
|
|
282
|
+
calendarObject: {
|
|
283
|
+
url: eventUrl,
|
|
284
|
+
etag: '' // Will be fetched
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return { success: true };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Format date for iCalendar
|
|
293
|
+
*/
|
|
294
|
+
function formatICalDate(date) {
|
|
295
|
+
return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Escape text for iCalendar
|
|
300
|
+
*/
|
|
301
|
+
function escapeICalText(text) {
|
|
302
|
+
if (!text) return '';
|
|
303
|
+
return text
|
|
304
|
+
.replace(/\\/g, '\\\\')
|
|
305
|
+
.replace(/;/g, '\\;')
|
|
306
|
+
.replace(/,/g, '\\,')
|
|
307
|
+
.replace(/\n/g, '\\n');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = {
|
|
311
|
+
getClient,
|
|
312
|
+
clearClient,
|
|
313
|
+
getCalendars,
|
|
314
|
+
listEvents,
|
|
315
|
+
createEvent,
|
|
316
|
+
updateEvent,
|
|
317
|
+
deleteEvent,
|
|
318
|
+
parseEvent,
|
|
319
|
+
applyICalChanges
|
|
320
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar module for iCloud MCP
|
|
3
|
+
* Provides calendar tools via CalDAV
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { z } = require('zod');
|
|
7
|
+
const cloudClient = require('./caldav-client');
|
|
8
|
+
const localClient = require('./local-client');
|
|
9
|
+
const { isLocalMode } = require('../mode');
|
|
10
|
+
const { formatSuccess, formatError, withErrorHandler } = require('../utils/error-handler');
|
|
11
|
+
const { listOutput, listResult } = require('../utils/schemas');
|
|
12
|
+
const { formatDate } = require('../utils/date-utils');
|
|
13
|
+
const config = require('../config');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Normalize an event to one shape regardless of mode.
|
|
17
|
+
*
|
|
18
|
+
* Local (Calendar.app) keys events by UID and returns ISO strings; cloud
|
|
19
|
+
* (CalDAV) keys them by URL and returns Date objects. Everything downstream
|
|
20
|
+
* sees `ref` (the handle you pass back to update/delete), `start`/`end` as
|
|
21
|
+
* Dates, and `calendarName`.
|
|
22
|
+
*/
|
|
23
|
+
function normalizeEvent(event, local) {
|
|
24
|
+
if (!local) return { ...event, ref: event.url };
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
summary: event.summary,
|
|
28
|
+
start: event.startDate ? new Date(event.startDate) : null,
|
|
29
|
+
end: event.endDate ? new Date(event.endDate) : null,
|
|
30
|
+
location: event.location || '',
|
|
31
|
+
calendarName: event.calendar,
|
|
32
|
+
isAllDay: false,
|
|
33
|
+
ref: event.id,
|
|
34
|
+
uid: event.id
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Normalize a calendar entry to one shape regardless of mode.
|
|
40
|
+
*/
|
|
41
|
+
function normalizeCalendar(calendar, local) {
|
|
42
|
+
if (!local) return { displayName: calendar.displayName, ref: calendar.url, writable: true };
|
|
43
|
+
return { displayName: calendar.name, ref: calendar.id, writable: calendar.writable };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Handler: List events
|
|
48
|
+
*/
|
|
49
|
+
async function handleListEvents(args) {
|
|
50
|
+
const count = Math.min(args.count || 25, config.DEFAULTS.MAX_RESULTS);
|
|
51
|
+
const daysAhead = args.daysAhead || 30;
|
|
52
|
+
|
|
53
|
+
const local = isLocalMode();
|
|
54
|
+
const client = local ? localClient : cloudClient;
|
|
55
|
+
|
|
56
|
+
const raw = await client.listEvents(count, daysAhead);
|
|
57
|
+
const events = raw.map(e => normalizeEvent(e, local));
|
|
58
|
+
|
|
59
|
+
if (events.length === 0) {
|
|
60
|
+
return formatSuccess(`No upcoming events in the next ${daysAhead} days.`, listResult([]));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lines = events.map((event, i) => {
|
|
64
|
+
const dateStr = event.isAllDay
|
|
65
|
+
? `All day: ${formatDate(event.start, { hour: undefined, minute: undefined })}`
|
|
66
|
+
: `${formatDate(event.start)} - ${formatDate(event.end, { year: undefined, month: undefined, day: undefined })}`;
|
|
67
|
+
|
|
68
|
+
let line = `${i + 1}. ${event.summary}\n ${dateStr}`;
|
|
69
|
+
if (event.location) line += `\n Location: ${event.location}`;
|
|
70
|
+
if (event.calendarName) line += `\n Calendar: ${event.calendarName}`;
|
|
71
|
+
line += `\n ${local ? 'ID' : 'URL'}: ${event.ref}`;
|
|
72
|
+
|
|
73
|
+
return line;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return formatSuccess(`Upcoming events (${events.length}):\n\n${lines.join('\n\n')}`, listResult(events));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handler: Create event
|
|
81
|
+
*/
|
|
82
|
+
async function handleCreateEvent(args) {
|
|
83
|
+
if (!args.summary) {
|
|
84
|
+
return formatError(new Error('Event summary/title is required'));
|
|
85
|
+
}
|
|
86
|
+
if (!args.start) {
|
|
87
|
+
return formatError(new Error('Start date/time is required (ISO 8601 format)'));
|
|
88
|
+
}
|
|
89
|
+
if (!args.end) {
|
|
90
|
+
return formatError(new Error('End date/time is required (ISO 8601 format)'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const local = isLocalMode();
|
|
94
|
+
const client = local ? localClient : cloudClient;
|
|
95
|
+
|
|
96
|
+
const result = await client.createEvent({
|
|
97
|
+
summary: args.summary,
|
|
98
|
+
start: args.start,
|
|
99
|
+
end: args.end,
|
|
100
|
+
description: args.description,
|
|
101
|
+
location: args.location,
|
|
102
|
+
calendarUrl: args.calendarUrl, // cloud
|
|
103
|
+
calendarName: args.calendarName // local
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return formatSuccess(
|
|
107
|
+
`Event created successfully!\n\nTitle: ${args.summary}\nStart: ${formatDate(new Date(args.start))}\nEnd: ${formatDate(new Date(args.end))}${args.location ? `\nLocation: ${args.location}` : ''}${result.calendar ? `\nCalendar: ${result.calendar}` : ''}\n${local ? 'ID' : 'UID'}: ${result.uid || result.id}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Handler: Update event
|
|
113
|
+
*/
|
|
114
|
+
async function handleUpdateEvent(args) {
|
|
115
|
+
if (!args.eventUrl) {
|
|
116
|
+
return formatError(new Error('Event URL is required (from list-events output)'));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const changes = {};
|
|
120
|
+
for (const field of ['summary', 'start', 'end', 'description', 'location']) {
|
|
121
|
+
if (args[field] !== undefined) changes[field] = args[field];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (Object.keys(changes).length === 0) {
|
|
125
|
+
return formatError(new Error('Nothing to update. Provide at least one of: summary, start, end, description, location.'));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const field of ['start', 'end']) {
|
|
129
|
+
if (changes[field] !== undefined && Number.isNaN(new Date(changes[field]).getTime())) {
|
|
130
|
+
return formatError(new Error(`Invalid ${field} date. Use ISO 8601, e.g. 2026-01-15T10:00:00`));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const local = isLocalMode();
|
|
135
|
+
const client = local ? localClient : cloudClient;
|
|
136
|
+
|
|
137
|
+
await client.updateEvent(args.eventUrl, changes);
|
|
138
|
+
|
|
139
|
+
const summary = Object.entries(changes)
|
|
140
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
141
|
+
.join('\n');
|
|
142
|
+
|
|
143
|
+
return formatSuccess(`Event updated successfully.\n\n${summary}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Handler: Delete event
|
|
148
|
+
*/
|
|
149
|
+
async function handleDeleteEvent(args) {
|
|
150
|
+
if (!args.eventUrl) {
|
|
151
|
+
return formatError(new Error('Event URL is required'));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const client = isLocalMode() ? localClient : cloudClient;
|
|
155
|
+
await client.deleteEvent(args.eventUrl);
|
|
156
|
+
|
|
157
|
+
return formatSuccess(`Event deleted successfully.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Handler: List calendars
|
|
162
|
+
*/
|
|
163
|
+
async function handleListCalendars() {
|
|
164
|
+
const local = isLocalMode();
|
|
165
|
+
const raw = local ? await localClient.listCalendars() : await cloudClient.getCalendars();
|
|
166
|
+
const calendars = raw.map(c => normalizeCalendar(c, local));
|
|
167
|
+
|
|
168
|
+
if (calendars.length === 0) {
|
|
169
|
+
return formatSuccess('No calendars found.', listResult([]));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const lines = calendars.map((cal, i) =>
|
|
173
|
+
`${i + 1}. ${cal.displayName}\n ${local ? 'ID' : 'URL'}: ${cal.ref}`
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return formatSuccess(`Calendars (${calendars.length}):\n\n${lines.join('\n\n')}`, listResult(calendars));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Tool definitions
|
|
180
|
+
const calendarTools = [
|
|
181
|
+
{
|
|
182
|
+
name: 'list-events',
|
|
183
|
+
outputSchema: listOutput('Calendar events'),
|
|
184
|
+
title: 'List Events',
|
|
185
|
+
description: 'Lists upcoming calendar events',
|
|
186
|
+
inputSchema: {
|
|
187
|
+
count: z.number().int().min(1).max(50).optional().describe('Number of events to retrieve (default: 25, max: 50)'),
|
|
188
|
+
daysAhead: z.number().int().min(1).max(365).optional().describe('Number of days to look ahead (default: 30)')
|
|
189
|
+
},
|
|
190
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":true},
|
|
191
|
+
handler: withErrorHandler(handleListEvents, 'list-events')
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'create-event',
|
|
195
|
+
title: 'Create Event',
|
|
196
|
+
description: 'Creates a new calendar event',
|
|
197
|
+
inputSchema: {
|
|
198
|
+
summary: z.string().describe('Event title/summary'),
|
|
199
|
+
start: z.string().describe('Start date/time in ISO 8601 format (e.g., 2026-01-15T10:00:00)'),
|
|
200
|
+
end: z.string().describe('End date/time in ISO 8601 format'),
|
|
201
|
+
description: z.string().optional().describe('Event description (optional)'),
|
|
202
|
+
location: z.string().optional().describe('Event location (optional)'),
|
|
203
|
+
calendarUrl: z.string().optional().describe('Cloud mode: URL of the calendar to add the event to (optional, uses the first calendar)'),
|
|
204
|
+
calendarName: z.string().optional().describe('Local mode: name of the calendar to add the event to (optional, uses the default)')
|
|
205
|
+
},
|
|
206
|
+
annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":true},
|
|
207
|
+
handler: withErrorHandler(handleCreateEvent, 'create-event')
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'update-event',
|
|
211
|
+
title: 'Update Event',
|
|
212
|
+
description: 'Updates an existing calendar event. Only the fields you pass are changed. In cloud mode the CalDAV property-merge preserves recurrence, invitees and alarms (experimental: the live round-trip is not yet verified).',
|
|
213
|
+
inputSchema: {
|
|
214
|
+
eventUrl: z.string().describe('Event handle from list-events: the URL in cloud mode, the UID in local mode'),
|
|
215
|
+
summary: z.string().optional().describe('New event title (optional)'),
|
|
216
|
+
start: z.string().optional().describe('New start date/time in ISO 8601 format (optional)'),
|
|
217
|
+
end: z.string().optional().describe('New end date/time in ISO 8601 format (optional)'),
|
|
218
|
+
description: z.string().optional().describe('New description (optional)'),
|
|
219
|
+
location: z.string().optional().describe('New location (optional)')
|
|
220
|
+
},
|
|
221
|
+
annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":true},
|
|
222
|
+
handler: withErrorHandler(handleUpdateEvent, 'update-event')
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'delete-event',
|
|
226
|
+
title: 'Delete Event',
|
|
227
|
+
description: 'Deletes a calendar event',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
eventUrl: z.string().describe('Event handle from list-events: the URL in cloud mode, the UID in local mode')
|
|
230
|
+
},
|
|
231
|
+
annotations: {"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":true},
|
|
232
|
+
handler: withErrorHandler(handleDeleteEvent, 'delete-event')
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'list-calendars',
|
|
236
|
+
outputSchema: listOutput('Calendars'),
|
|
237
|
+
title: 'List Calendars',
|
|
238
|
+
description: 'Lists all available calendars',
|
|
239
|
+
inputSchema: {},
|
|
240
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":true},
|
|
241
|
+
handler: withErrorHandler(handleListCalendars, 'list-calendars')
|
|
242
|
+
}
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
calendarTools,
|
|
247
|
+
handleListEvents,
|
|
248
|
+
handleCreateEvent,
|
|
249
|
+
handleUpdateEvent,
|
|
250
|
+
handleDeleteEvent,
|
|
251
|
+
handleListCalendars
|
|
252
|
+
};
|