icloud-mcp 2.5.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +235 -341
- package/index.js +30 -1138
- package/lib/caldav.js +628 -3
- package/lib/carddav.js +28 -2
- package/lib/data-paths.js +13 -0
- package/lib/digest.js +23 -8
- package/lib/imap.js +298 -85
- package/lib/reminders.js +316 -0
- package/lib/session.js +9 -7
- package/lib/smtp.js +53 -21
- package/lib/tools/calendar.js +333 -0
- package/lib/tools/contacts.js +135 -0
- package/lib/tools/mail.js +903 -0
- package/lib/tools/reminders.js +165 -0
- package/package.json +26 -7
- package/.claude/scheduled_tasks.lock +0 -1
- package/.claude/settings.local.json +0 -35
- package/.mcp.json.example +0 -12
- package/run-digest.js +0 -57
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// iCloud Calendar (CalDAV) tools, including email-to-event suggestion.
|
|
2
|
+
import { TIMEOUT, withTimeout, getEmailContent } from '../imap.js';
|
|
3
|
+
import { formatEmailForExtraction } from '../event-extractor.js';
|
|
4
|
+
import {
|
|
5
|
+
listCalendars, createCalendar, deleteCalendar, listEvents, getEvent, createEvent, updateEvent, deleteEvent, searchEvents,
|
|
6
|
+
bulkCreateEvents, bulkDeleteEvents, bulkUpdateEvents, listEventsMulti, detectConflicts,
|
|
7
|
+
} from '../caldav.js';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export const calendarTools = [
|
|
11
|
+
// ── CalDAV / Calendar ──
|
|
12
|
+
{
|
|
13
|
+
name: 'list_calendars',
|
|
14
|
+
description: 'List all calendars in iCloud Calendar (e.g. Personal, Work, School). Returns calendarId, name, and supported event types.',
|
|
15
|
+
inputSchema: { type: 'object', properties: {} }
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'create_calendar',
|
|
19
|
+
description: 'Create a new event calendar in iCloud Calendar. Fails if a calendar with that name already exists.',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
name: { type: 'string', description: 'Name of the new calendar' },
|
|
24
|
+
},
|
|
25
|
+
required: ['name']
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'delete_calendar',
|
|
30
|
+
description: 'Delete an iCloud event calendar by exact name or calendarId. The calendar must have no events (past or future); delete them first with bulk_delete_events. Will not delete Reminders lists.',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
name: { type: 'string', description: 'Exact calendar name (must match exactly one calendar)' },
|
|
35
|
+
calendarId: { type: 'string', description: 'Calendar ID from list_calendars (use instead of name)' },
|
|
36
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'list_events',
|
|
42
|
+
description: 'List events in a specific iCloud calendar within a date range. Use list_calendars first to get a calendarId.',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
calendarId: { type: 'string', description: 'Calendar ID from list_calendars' },
|
|
47
|
+
since: { type: 'string', description: 'Start of range (YYYY-MM-DD, default: 30 days ago)' },
|
|
48
|
+
before: { type: 'string', description: 'End of range (YYYY-MM-DD, default: 30 days ahead)' },
|
|
49
|
+
limit: { type: 'number', description: 'Max events to return (default 50)' }
|
|
50
|
+
},
|
|
51
|
+
required: ['calendarId']
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'get_event',
|
|
56
|
+
description: 'Get full details of a specific calendar event by its ID.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
calendarId: { type: 'string', description: 'Calendar ID containing the event' },
|
|
61
|
+
eventId: { type: 'string', description: 'Event ID (UUID from list_events or search_events)' }
|
|
62
|
+
},
|
|
63
|
+
required: ['calendarId', 'eventId']
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'create_event',
|
|
68
|
+
description: 'Create a new event in an iCloud calendar. For all-day events use allDay:true and YYYY-MM-DD for start/end.',
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
calendarId: { type: 'string', description: 'Calendar ID to add the event to' },
|
|
73
|
+
summary: { type: 'string', description: 'Event title' },
|
|
74
|
+
start: { type: 'string', description: 'Start date/time — ISO 8601 (e.g. 2026-03-15T10:00:00) or YYYY-MM-DD for all-day' },
|
|
75
|
+
end: { type: 'string', description: 'End date/time — ISO 8601 or YYYY-MM-DD. Defaults to 1 hour after start.' },
|
|
76
|
+
timezone: { type: 'string', description: 'IANA timezone (e.g. America/New_York). Use "UTC" or omit for UTC.' },
|
|
77
|
+
allDay: { type: 'boolean', description: 'True for all-day event (uses DATE values, no time)' },
|
|
78
|
+
description: { type: 'string', description: 'Event description / notes' },
|
|
79
|
+
location: { type: 'string', description: 'Event location' },
|
|
80
|
+
recurrence: { type: 'string', description: 'iCal RRULE string (e.g. FREQ=WEEKLY;BYDAY=MO,WE,FR)' },
|
|
81
|
+
status: { type: 'string', description: 'Event status: CONFIRMED, TENTATIVE, or CANCELLED' },
|
|
82
|
+
reminder: { type: 'number', description: 'Alert this many minutes before the event (default 30, set to 0 to disable)' }
|
|
83
|
+
},
|
|
84
|
+
required: ['calendarId', 'summary', 'start']
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'update_event',
|
|
89
|
+
description: 'Update an existing calendar event. Only provided fields are changed; others are preserved.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
calendarId: { type: 'string', description: 'Calendar ID containing the event' },
|
|
94
|
+
eventId: { type: 'string', description: 'Event ID to update' },
|
|
95
|
+
summary: { type: 'string' },
|
|
96
|
+
start: { type: 'string' },
|
|
97
|
+
end: { type: 'string' },
|
|
98
|
+
timezone: { type: 'string' },
|
|
99
|
+
allDay: { type: 'boolean' },
|
|
100
|
+
description: { type: 'string' },
|
|
101
|
+
location: { type: 'string' },
|
|
102
|
+
recurrence: { type: 'string' },
|
|
103
|
+
status: { type: 'string' },
|
|
104
|
+
reminder: { type: 'number', description: 'Alert minutes before event (0 to disable)' }
|
|
105
|
+
},
|
|
106
|
+
required: ['calendarId', 'eventId']
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'delete_event',
|
|
111
|
+
description: 'Delete a calendar event permanently from iCloud Calendar.',
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
calendarId: { type: 'string', description: 'Calendar ID containing the event' },
|
|
116
|
+
eventId: { type: 'string', description: 'Event ID to delete' },
|
|
117
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
118
|
+
},
|
|
119
|
+
required: ['calendarId', 'eventId']
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'search_events',
|
|
124
|
+
description: 'Search for events by title/summary across all calendars within an optional date range.',
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
query: { type: 'string', description: 'Text to search for in event titles' },
|
|
129
|
+
since: { type: 'string', description: 'Start of search range (YYYY-MM-DD, default: 1 year ago)' },
|
|
130
|
+
before: { type: 'string', description: 'End of search range (YYYY-MM-DD, default: 1 year ahead)' }
|
|
131
|
+
},
|
|
132
|
+
required: ['query']
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'bulk_update_events',
|
|
137
|
+
description: 'Update multiple calendar events in one call. Each update object must include eventId and only the fields to change.',
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
calendarId: { type: 'string', description: 'Calendar ID containing the events' },
|
|
142
|
+
updates: {
|
|
143
|
+
type: 'array',
|
|
144
|
+
description: 'Array of update objects, each with eventId (required) plus any fields to change: summary, start, end, timezone, allDay, description, location, recurrence, status, reminder',
|
|
145
|
+
items: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
eventId: { type: 'string' },
|
|
149
|
+
summary: { type: 'string' },
|
|
150
|
+
start: { type: 'string' },
|
|
151
|
+
end: { type: 'string' },
|
|
152
|
+
timezone: { type: 'string' },
|
|
153
|
+
allDay: { type: 'boolean' },
|
|
154
|
+
description: { type: 'string' },
|
|
155
|
+
location: { type: 'string' },
|
|
156
|
+
recurrence: { type: 'string' },
|
|
157
|
+
status: { type: 'string' },
|
|
158
|
+
reminder: { type: 'number' }
|
|
159
|
+
},
|
|
160
|
+
required: ['eventId']
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
164
|
+
},
|
|
165
|
+
required: ['calendarId', 'updates']
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'list_events_multi',
|
|
170
|
+
description: 'List events from multiple calendars in one call. Returns events grouped by calendar ID.',
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
calendarIds: {
|
|
175
|
+
type: 'array',
|
|
176
|
+
description: 'Array of calendar IDs to fetch events from',
|
|
177
|
+
items: { type: 'string' }
|
|
178
|
+
},
|
|
179
|
+
since: { type: 'string', description: 'Start of date range (YYYY-MM-DD, default: 30 days ago)' },
|
|
180
|
+
before: { type: 'string', description: 'End of date range (YYYY-MM-DD, default: 30 days ahead)' },
|
|
181
|
+
limit: { type: 'number', description: 'Max events per calendar (default 50)' }
|
|
182
|
+
},
|
|
183
|
+
required: ['calendarIds']
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'bulk_create_events',
|
|
188
|
+
description: 'Create multiple calendar events in one call. Much more efficient than calling create_event repeatedly. Each event in the array uses the same fields as create_event.',
|
|
189
|
+
inputSchema: {
|
|
190
|
+
type: 'object',
|
|
191
|
+
properties: {
|
|
192
|
+
calendarId: { type: 'string', description: 'Calendar ID to add all events to' },
|
|
193
|
+
events: {
|
|
194
|
+
type: 'array',
|
|
195
|
+
description: 'Array of event objects, each with: summary (required), start (required), end, timezone, allDay, description, location, recurrence, status, reminder',
|
|
196
|
+
items: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
summary: { type: 'string' },
|
|
200
|
+
start: { type: 'string' },
|
|
201
|
+
end: { type: 'string' },
|
|
202
|
+
timezone: { type: 'string' },
|
|
203
|
+
allDay: { type: 'boolean' },
|
|
204
|
+
description: { type: 'string' },
|
|
205
|
+
location: { type: 'string' },
|
|
206
|
+
recurrence: { type: 'string' },
|
|
207
|
+
status: { type: 'string' },
|
|
208
|
+
reminder: { type: 'number' }
|
|
209
|
+
},
|
|
210
|
+
required: ['summary', 'start']
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
214
|
+
},
|
|
215
|
+
required: ['calendarId', 'events']
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'bulk_delete_events',
|
|
220
|
+
description: 'Delete multiple calendar events in one call. Much more efficient than calling delete_event repeatedly.',
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {
|
|
224
|
+
calendarId: { type: 'string', description: 'Calendar ID containing the events' },
|
|
225
|
+
eventIds: {
|
|
226
|
+
type: 'array',
|
|
227
|
+
description: 'Array of event IDs to delete',
|
|
228
|
+
items: { type: 'string' }
|
|
229
|
+
},
|
|
230
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
231
|
+
},
|
|
232
|
+
required: ['calendarId', 'eventIds']
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'detect_conflicts',
|
|
237
|
+
description: 'Detect scheduling conflicts and tight gaps between events across multiple calendars. Compares all non-all-day events on the same date from different calendars.',
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
properties: {
|
|
241
|
+
calendarIds: {
|
|
242
|
+
type: 'array',
|
|
243
|
+
description: 'Array of calendar IDs to check for conflicts',
|
|
244
|
+
items: { type: 'string' }
|
|
245
|
+
},
|
|
246
|
+
since: { type: 'string', description: 'Start of range (YYYY-MM-DD, default: today)' },
|
|
247
|
+
before: { type: 'string', description: 'End of range (YYYY-MM-DD, default: 90 days ahead)' },
|
|
248
|
+
minBuffer: { type: 'number', description: 'Minimum buffer minutes between events to flag tight gaps (default 0 = no gap check)' }
|
|
249
|
+
},
|
|
250
|
+
required: ['calendarIds']
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
export const suggestEventTools = [
|
|
256
|
+
// ── Smart extraction ──
|
|
257
|
+
{
|
|
258
|
+
name: 'suggest_event_from_email',
|
|
259
|
+
description: 'Fetch an email and return its content formatted for calendar event extraction. After calling this tool, extract the event fields from the returned content (pay attention to _dateAnchor for resolving relative dates like "Tuesday"), present a summary to the user for confirmation, then call create_event. No API key required.',
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: 'object',
|
|
262
|
+
properties: {
|
|
263
|
+
uid: { type: 'number', description: 'Email UID to extract event from' },
|
|
264
|
+
mailbox: { type: 'string', description: 'Mailbox containing the email (default INBOX)' }
|
|
265
|
+
},
|
|
266
|
+
required: ['uid']
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const _calendarTools_NAMES = new Set([...calendarTools, ...suggestEventTools].map((tool) => tool.name));
|
|
272
|
+
|
|
273
|
+
export async function handleCalendarTool(name, args, ctx) {
|
|
274
|
+
if (!_calendarTools_NAMES.has(name)) return undefined;
|
|
275
|
+
const { resolveCreds, resolveMailbox, accounts: ACCOUNTS } = ctx;
|
|
276
|
+
let result;
|
|
277
|
+
if (name === 'list_calendars') {
|
|
278
|
+
result = await withTimeout('list_calendars', TIMEOUT.FETCH, () =>
|
|
279
|
+
listCalendars()
|
|
280
|
+
);
|
|
281
|
+
} else if (name === 'create_calendar') {
|
|
282
|
+
result = await withTimeout('create_calendar', TIMEOUT.FETCH, () => createCalendar(args.name));
|
|
283
|
+
} else if (name === 'delete_calendar') {
|
|
284
|
+
result = await withTimeout('delete_calendar', TIMEOUT.FETCH, () =>
|
|
285
|
+
deleteCalendar({ name: args.name, calendarId: args.calendarId }, args.dryRun || false)
|
|
286
|
+
);
|
|
287
|
+
} else if (name === 'list_events') {
|
|
288
|
+
result = await withTimeout('list_events', TIMEOUT.FETCH, () =>
|
|
289
|
+
listEvents(args.calendarId, args.since || null, args.before || null, args.limit || 50)
|
|
290
|
+
);
|
|
291
|
+
} else if (name === 'get_event') {
|
|
292
|
+
result = await withTimeout('get_event', TIMEOUT.FETCH, () =>
|
|
293
|
+
getEvent(args.calendarId, args.eventId)
|
|
294
|
+
);
|
|
295
|
+
} else if (name === 'create_event') {
|
|
296
|
+
const { calendarId, ...fields } = args;
|
|
297
|
+
result = await withTimeout('create_event', TIMEOUT.FETCH, () =>
|
|
298
|
+
createEvent(calendarId, fields)
|
|
299
|
+
);
|
|
300
|
+
} else if (name === 'update_event') {
|
|
301
|
+
const { calendarId, eventId, ...fields } = args;
|
|
302
|
+
result = await withTimeout('update_event', TIMEOUT.FETCH, () =>
|
|
303
|
+
updateEvent(calendarId, eventId, fields)
|
|
304
|
+
);
|
|
305
|
+
} else if (name === 'delete_event') {
|
|
306
|
+
result = await withTimeout('delete_event', TIMEOUT.SINGLE, () =>
|
|
307
|
+
deleteEvent(args.calendarId, args.eventId, args.dryRun || false)
|
|
308
|
+
);
|
|
309
|
+
} else if (name === 'search_events') {
|
|
310
|
+
result = await withTimeout('search_events', TIMEOUT.FETCH, () =>
|
|
311
|
+
searchEvents(args.query, args.since || null, args.before || null)
|
|
312
|
+
);
|
|
313
|
+
} else if (name === 'bulk_update_events') {
|
|
314
|
+
result = await bulkUpdateEvents(args.calendarId, args.updates, args.dryRun || false);
|
|
315
|
+
} else if (name === 'list_events_multi') {
|
|
316
|
+
result = await listEventsMulti(args.calendarIds, args.since || null, args.before || null, args.limit || 50);
|
|
317
|
+
} else if (name === 'bulk_create_events') {
|
|
318
|
+
result = await bulkCreateEvents(args.calendarId, args.events, args.dryRun || false);
|
|
319
|
+
} else if (name === 'bulk_delete_events') {
|
|
320
|
+
result = await bulkDeleteEvents(args.calendarId, args.eventIds, args.dryRun || false);
|
|
321
|
+
} else if (name === 'detect_conflicts') {
|
|
322
|
+
result = await detectConflicts(args.calendarIds, args.since || null, args.before || null, args.minBuffer || 0);
|
|
323
|
+
// ── Reminders / JXA (synchronous osascript — no timeout needed) ──
|
|
324
|
+
} else if (name === 'suggest_event_from_email') {
|
|
325
|
+
const creds = resolveCreds(args.account);
|
|
326
|
+
const email = await withTimeout('get_email_for_extraction', TIMEOUT.FETCH, () =>
|
|
327
|
+
getEmailContent(args.uid, resolveMailbox(args.mailbox || 'INBOX', creds), 10000, false, creds)
|
|
328
|
+
);
|
|
329
|
+
result = formatEmailForExtraction(email);
|
|
330
|
+
}
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// iCloud Contacts (CardDAV) tools.
|
|
2
|
+
import { TIMEOUT, withTimeout } from '../imap.js';
|
|
3
|
+
import { listContacts, searchContacts, getContact, createContact, updateContact, deleteContact } from '../carddav.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export const contactTools = [
|
|
7
|
+
// ── CardDAV / Contacts ──
|
|
8
|
+
{
|
|
9
|
+
name: 'list_contacts',
|
|
10
|
+
description: 'List contacts from iCloud Contacts. Returns names, phones, emails, and other fields.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
limit: { type: 'number', description: 'Max contacts to return (default 50)' },
|
|
15
|
+
offset: { type: 'number', description: 'Skip this many contacts (default 0, for pagination)' }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'search_contacts',
|
|
21
|
+
description: 'Search iCloud Contacts by name, email address, or phone number.',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
query: { type: 'string', description: 'Text to search for (matched against name, email, and phone)' }
|
|
26
|
+
},
|
|
27
|
+
required: ['query']
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'get_contact',
|
|
32
|
+
description: 'Get full details for a specific contact by ID. Use list_contacts or search_contacts to find a contactId.',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
contactId: { type: 'string', description: 'Contact ID (UUID from list_contacts or search_contacts)' }
|
|
37
|
+
},
|
|
38
|
+
required: ['contactId']
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'create_contact',
|
|
43
|
+
description: 'Create a new contact in iCloud Contacts.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
firstName: { type: 'string', description: 'First name' },
|
|
48
|
+
lastName: { type: 'string', description: 'Last name' },
|
|
49
|
+
fullName: { type: 'string', description: 'Full display name (overrides firstName + lastName for FN field)' },
|
|
50
|
+
org: { type: 'string', description: 'Organization / company name' },
|
|
51
|
+
phone: { type: 'string', description: 'Primary phone number (shorthand for phones array)' },
|
|
52
|
+
email: { type: 'string', description: 'Primary email address (shorthand for emails array)' },
|
|
53
|
+
phones: { type: 'array', description: 'Array of phone objects: [{ number, type }] where type is cell/home/work/etc.' },
|
|
54
|
+
emails: { type: 'array', description: 'Array of email objects: [{ email, type }] where type is home/work/etc.' },
|
|
55
|
+
addresses: { type: 'array', description: 'Array of address objects: [{ street, city, state, zip, country, type }]' },
|
|
56
|
+
birthday: { type: 'string', description: 'Birthday in YYYY-MM-DD format' },
|
|
57
|
+
note: { type: 'string', description: 'Notes / free text' },
|
|
58
|
+
url: { type: 'string', description: 'Website URL' }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'update_contact',
|
|
64
|
+
description: 'Update an existing contact in iCloud Contacts. Only provided fields are changed; others are preserved.',
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
contactId: { type: 'string', description: 'Contact ID to update' },
|
|
69
|
+
firstName: { type: 'string' },
|
|
70
|
+
lastName: { type: 'string' },
|
|
71
|
+
fullName: { type: 'string' },
|
|
72
|
+
org: { type: 'string' },
|
|
73
|
+
phone: { type: 'string' },
|
|
74
|
+
email: { type: 'string' },
|
|
75
|
+
phones: { type: 'array' },
|
|
76
|
+
emails: { type: 'array' },
|
|
77
|
+
addresses: { type: 'array' },
|
|
78
|
+
birthday: { type: 'string' },
|
|
79
|
+
note: { type: 'string' },
|
|
80
|
+
url: { type: 'string' }
|
|
81
|
+
},
|
|
82
|
+
required: ['contactId']
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'delete_contact',
|
|
87
|
+
description: 'Delete a contact from iCloud Contacts permanently.',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
contactId: { type: 'string', description: 'Contact ID to delete' },
|
|
92
|
+
dryRun: { type: 'boolean', description: 'If true, report exactly what would change and do not modify anything' },
|
|
93
|
+
},
|
|
94
|
+
required: ['contactId']
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const _contactTools_NAMES = new Set(contactTools.map((tool) => tool.name));
|
|
100
|
+
|
|
101
|
+
export async function handleContactTool(name, args, ctx) {
|
|
102
|
+
if (!_contactTools_NAMES.has(name)) return undefined;
|
|
103
|
+
const { resolveCreds, resolveMailbox, accounts: ACCOUNTS } = ctx;
|
|
104
|
+
let result;
|
|
105
|
+
if (name === 'list_contacts') {
|
|
106
|
+
result = await withTimeout('list_contacts', TIMEOUT.FETCH, () =>
|
|
107
|
+
listContacts(args.limit || 50, args.offset || 0)
|
|
108
|
+
);
|
|
109
|
+
} else if (name === 'search_contacts') {
|
|
110
|
+
result = await withTimeout('search_contacts', TIMEOUT.FETCH, () =>
|
|
111
|
+
searchContacts(args.query)
|
|
112
|
+
);
|
|
113
|
+
} else if (name === 'get_contact') {
|
|
114
|
+
result = await withTimeout('get_contact', TIMEOUT.FETCH, () =>
|
|
115
|
+
getContact(args.contactId)
|
|
116
|
+
);
|
|
117
|
+
} else if (name === 'create_contact') {
|
|
118
|
+
const { contactId: _ignore, ...fields } = args;
|
|
119
|
+
result = await withTimeout('create_contact', TIMEOUT.FETCH, () =>
|
|
120
|
+
createContact(fields)
|
|
121
|
+
);
|
|
122
|
+
} else if (name === 'update_contact') {
|
|
123
|
+
const { contactId, ...fields } = args;
|
|
124
|
+
result = await withTimeout('update_contact', TIMEOUT.FETCH, () =>
|
|
125
|
+
updateContact(contactId, fields)
|
|
126
|
+
);
|
|
127
|
+
} else if (name === 'delete_contact') {
|
|
128
|
+
result = await withTimeout('delete_contact', TIMEOUT.SINGLE, () =>
|
|
129
|
+
deleteContact(args.contactId, args.dryRun || false)
|
|
130
|
+
);
|
|
131
|
+
// ── CalDAV / Calendar (FETCH tier 30s) ──
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|