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,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contacts module for iCloud MCP
|
|
3
|
+
* Provides contacts tools via CardDAV (cloud) or AppleScript (local)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { z } = require('zod');
|
|
7
|
+
const cloudClient = require('./carddav-client');
|
|
8
|
+
const localClient = require('./local-client');
|
|
9
|
+
const { formatSuccess, formatError, withErrorHandler } = require('../utils/error-handler');
|
|
10
|
+
const { listOutput, listResult } = require('../utils/schemas');
|
|
11
|
+
const { isLocalMode } = require('../mode');
|
|
12
|
+
const config = require('../config');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the appropriate client based on current mode
|
|
16
|
+
*/
|
|
17
|
+
function getClient() {
|
|
18
|
+
return isLocalMode() ? localClient : cloudClient;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize contact from local format to common format
|
|
23
|
+
*/
|
|
24
|
+
function normalizeLocalContact(contact) {
|
|
25
|
+
return {
|
|
26
|
+
displayName: contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
|
|
27
|
+
firstName: contact.firstName,
|
|
28
|
+
lastName: contact.lastName,
|
|
29
|
+
emails: contact.email ? [{ value: contact.email, type: 'work' }] : (contact.emails || []),
|
|
30
|
+
phones: contact.phone ? [{ value: contact.phone, type: 'mobile' }] : (contact.phones || []),
|
|
31
|
+
organization: contact.organization,
|
|
32
|
+
title: contact.jobTitle,
|
|
33
|
+
notes: contact.note,
|
|
34
|
+
url: contact.id, // Use ID as URL for local contacts
|
|
35
|
+
uid: contact.id
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalize contact list based on mode
|
|
41
|
+
*/
|
|
42
|
+
function normalizeContacts(contacts, isLocal) {
|
|
43
|
+
if (!isLocal) return contacts;
|
|
44
|
+
return contacts.map(normalizeLocalContact);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Handler: List contacts
|
|
49
|
+
*/
|
|
50
|
+
async function handleListContacts(args) {
|
|
51
|
+
const count = Math.min(args.count || 25, config.DEFAULTS.MAX_RESULTS);
|
|
52
|
+
const local = isLocalMode();
|
|
53
|
+
const client = getClient();
|
|
54
|
+
|
|
55
|
+
const rawContacts = await client.listContacts(count);
|
|
56
|
+
const contacts = normalizeContacts(rawContacts, local);
|
|
57
|
+
|
|
58
|
+
if (contacts.length === 0) {
|
|
59
|
+
return formatSuccess('No contacts found.', listResult([]));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const lines = contacts.map((contact, i) => {
|
|
63
|
+
let line = `${i + 1}. ${contact.displayName}`;
|
|
64
|
+
|
|
65
|
+
if (contact.emails && contact.emails.length > 0) {
|
|
66
|
+
line += `\n Email: ${contact.emails[0].value}`;
|
|
67
|
+
}
|
|
68
|
+
if (contact.phones && contact.phones.length > 0) {
|
|
69
|
+
line += `\n Phone: ${contact.phones[0].value}`;
|
|
70
|
+
}
|
|
71
|
+
if (contact.organization) {
|
|
72
|
+
line += `\n Company: ${contact.organization}`;
|
|
73
|
+
}
|
|
74
|
+
line += `\n ${local ? 'ID' : 'URL'}: ${contact.url}`;
|
|
75
|
+
|
|
76
|
+
return line;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return formatSuccess(`Contacts (${contacts.length}):\n\n${lines.join('\n\n')}`, listResult(contacts));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handler: Search contacts
|
|
84
|
+
*/
|
|
85
|
+
async function handleSearchContacts(args) {
|
|
86
|
+
if (!args.query) {
|
|
87
|
+
return formatError(new Error('Search query is required'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const count = Math.min(args.count || 25, config.DEFAULTS.MAX_RESULTS);
|
|
91
|
+
const local = isLocalMode();
|
|
92
|
+
const client = getClient();
|
|
93
|
+
|
|
94
|
+
const rawContacts = await client.searchContacts(args.query, count);
|
|
95
|
+
const contacts = normalizeContacts(rawContacts, local);
|
|
96
|
+
|
|
97
|
+
if (contacts.length === 0) {
|
|
98
|
+
return formatSuccess(`No contacts found matching "${args.query}".`, listResult([]));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const lines = contacts.map((contact, i) => {
|
|
102
|
+
let line = `${i + 1}. ${contact.displayName}`;
|
|
103
|
+
|
|
104
|
+
if (contact.emails && contact.emails.length > 0) {
|
|
105
|
+
line += `\n Email: ${contact.emails[0].value}`;
|
|
106
|
+
}
|
|
107
|
+
if (contact.phones && contact.phones.length > 0) {
|
|
108
|
+
line += `\n Phone: ${contact.phones[0].value}`;
|
|
109
|
+
}
|
|
110
|
+
if (contact.organization) {
|
|
111
|
+
line += `\n Company: ${contact.organization}`;
|
|
112
|
+
}
|
|
113
|
+
line += `\n ${local ? 'ID' : 'URL'}: ${contact.url}`;
|
|
114
|
+
|
|
115
|
+
return line;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return formatSuccess(`Search results for "${args.query}" (${contacts.length}):\n\n${lines.join('\n\n')}`, listResult(contacts));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handler: Read contact
|
|
123
|
+
*/
|
|
124
|
+
async function handleReadContact(args) {
|
|
125
|
+
if (!args.contactUrl) {
|
|
126
|
+
return formatError(new Error('Contact URL/ID is required'));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const local = isLocalMode();
|
|
130
|
+
const client = getClient();
|
|
131
|
+
|
|
132
|
+
// Local uses readContact, cloud uses getContact
|
|
133
|
+
const rawContact = local
|
|
134
|
+
? await client.readContact(args.contactUrl)
|
|
135
|
+
: await client.getContact(args.contactUrl);
|
|
136
|
+
|
|
137
|
+
const contact = local ? normalizeLocalContact(rawContact) : rawContact;
|
|
138
|
+
|
|
139
|
+
const emailList = contact.emails && contact.emails.length > 0
|
|
140
|
+
? contact.emails.map(e => ` - ${e.value} (${e.type || e.label || 'other'})`).join('\n')
|
|
141
|
+
: ' (none)';
|
|
142
|
+
|
|
143
|
+
const phoneList = contact.phones && contact.phones.length > 0
|
|
144
|
+
? contact.phones.map(p => ` - ${p.value} (${p.type || p.label || 'other'})`).join('\n')
|
|
145
|
+
: ' (none)';
|
|
146
|
+
|
|
147
|
+
return formatSuccess(
|
|
148
|
+
`Contact Details:
|
|
149
|
+
|
|
150
|
+
Name: ${contact.displayName}
|
|
151
|
+
First Name: ${contact.firstName || '(not set)'}
|
|
152
|
+
Last Name: ${contact.lastName || '(not set)'}
|
|
153
|
+
|
|
154
|
+
Emails:
|
|
155
|
+
${emailList}
|
|
156
|
+
|
|
157
|
+
Phones:
|
|
158
|
+
${phoneList}
|
|
159
|
+
|
|
160
|
+
Organization: ${contact.organization || '(not set)'}
|
|
161
|
+
Title: ${contact.title || '(not set)'}
|
|
162
|
+
|
|
163
|
+
Notes: ${contact.notes || '(none)'}
|
|
164
|
+
|
|
165
|
+
${local ? 'ID' : 'URL'}: ${contact.url}
|
|
166
|
+
UID: ${contact.uid}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handler: Create contact
|
|
172
|
+
*/
|
|
173
|
+
async function handleCreateContact(args) {
|
|
174
|
+
if (!args.displayName && !args.firstName && !args.lastName) {
|
|
175
|
+
return formatError(new Error('At least displayName or firstName/lastName is required'));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const local = isLocalMode();
|
|
179
|
+
const client = getClient();
|
|
180
|
+
|
|
181
|
+
const result = await client.createContact({
|
|
182
|
+
displayName: args.displayName,
|
|
183
|
+
firstName: args.firstName,
|
|
184
|
+
lastName: args.lastName,
|
|
185
|
+
email: args.email,
|
|
186
|
+
phone: args.phone,
|
|
187
|
+
organization: args.organization,
|
|
188
|
+
jobTitle: args.title, // Local uses jobTitle
|
|
189
|
+
title: args.title, // Cloud uses title
|
|
190
|
+
note: args.notes, // Local uses note
|
|
191
|
+
notes: args.notes // Cloud uses notes
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const name = args.displayName || `${args.firstName || ''} ${args.lastName || ''}`.trim();
|
|
195
|
+
|
|
196
|
+
return formatSuccess(
|
|
197
|
+
`Contact created successfully!\n\nName: ${name}${args.email ? `\nEmail: ${args.email}` : ''}${args.phone ? `\nPhone: ${args.phone}` : ''}${args.organization ? `\nOrganization: ${args.organization}` : ''}\n${local ? 'ID' : 'UID'}: ${result.uid || result.id}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Handler: Delete contact
|
|
203
|
+
*/
|
|
204
|
+
async function handleDeleteContact(args) {
|
|
205
|
+
if (!args.contactUrl) {
|
|
206
|
+
return formatError(new Error('Contact URL/ID is required'));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const client = getClient();
|
|
210
|
+
await client.deleteContact(args.contactUrl);
|
|
211
|
+
|
|
212
|
+
return formatSuccess('Contact deleted successfully.');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Handler: List contact accounts (LOCAL mode only)
|
|
217
|
+
*/
|
|
218
|
+
async function handleListContactAccounts(args) {
|
|
219
|
+
if (!isLocalMode()) {
|
|
220
|
+
return formatError(new Error('list-contact-accounts is only available in LOCAL mode'));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const accounts = await localClient.listAccounts();
|
|
224
|
+
|
|
225
|
+
if (accounts.length === 0) {
|
|
226
|
+
return formatSuccess('No contact accounts found.', listResult([]));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const lines = accounts.map((account, i) => {
|
|
230
|
+
return `${i + 1}. ${account.name}\n Groups: ${account.groupCount}\n ID: ${account.id}`;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return formatSuccess(`Contact Accounts (${accounts.length}):\n\n${lines.join('\n\n')}`, listResult(accounts));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Handler: List contact groups (LOCAL mode only)
|
|
238
|
+
*/
|
|
239
|
+
async function handleListContactGroups(args) {
|
|
240
|
+
if (!isLocalMode()) {
|
|
241
|
+
return formatError(new Error('list-contact-groups is only available in LOCAL mode'));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const groups = await localClient.listGroups(args.accountId || null);
|
|
245
|
+
|
|
246
|
+
if (groups.length === 0) {
|
|
247
|
+
return formatSuccess('No contact groups found.', listResult([]));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const lines = groups.map((group, i) => {
|
|
251
|
+
return `${i + 1}. ${group.name}\n Account: ${group.accountName}\n Contacts: ${group.contactCount}\n ID: ${group.id}`;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return formatSuccess(`Contact Groups (${groups.length}):\n\n${lines.join('\n\n')}`, listResult(groups));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Tool definitions
|
|
258
|
+
const contactsTools = [
|
|
259
|
+
{
|
|
260
|
+
name: 'list-contacts',
|
|
261
|
+
outputSchema: listOutput('Contacts'),
|
|
262
|
+
title: 'List Contacts',
|
|
263
|
+
description: 'Lists contacts from your iCloud address book',
|
|
264
|
+
inputSchema: {
|
|
265
|
+
count: z.number().int().min(1).max(50).optional().describe('Number of contacts to retrieve (default: 25, max: 50)')
|
|
266
|
+
},
|
|
267
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
|
|
268
|
+
handler: withErrorHandler(handleListContacts, 'list-contacts')
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'search-contacts',
|
|
272
|
+
outputSchema: listOutput('Matching contacts'),
|
|
273
|
+
title: 'Search Contacts',
|
|
274
|
+
description: 'Search contacts by name, email, or phone',
|
|
275
|
+
inputSchema: {
|
|
276
|
+
query: z.string().describe('Search query (name, email, or phone)'),
|
|
277
|
+
count: z.number().int().min(1).max(50).optional().describe('Max results (default: 25, max: 50)')
|
|
278
|
+
},
|
|
279
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
|
|
280
|
+
handler: withErrorHandler(handleSearchContacts, 'search-contacts')
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: 'read-contact',
|
|
284
|
+
title: 'Read Contact',
|
|
285
|
+
description: 'Get detailed information about a specific contact',
|
|
286
|
+
inputSchema: {
|
|
287
|
+
contactUrl: z.string().describe('URL or ID of the contact (from list-contacts output)')
|
|
288
|
+
},
|
|
289
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
|
|
290
|
+
handler: withErrorHandler(handleReadContact, 'read-contact')
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'create-contact',
|
|
294
|
+
title: 'Create Contact',
|
|
295
|
+
description: 'Creates a new contact',
|
|
296
|
+
inputSchema: {
|
|
297
|
+
displayName: z.string().optional().describe('Full display name'),
|
|
298
|
+
firstName: z.string().optional().describe('First name'),
|
|
299
|
+
lastName: z.string().optional().describe('Last name'),
|
|
300
|
+
email: z.string().optional().describe('Email address'),
|
|
301
|
+
phone: z.string().optional().describe('Phone number'),
|
|
302
|
+
organization: z.string().optional().describe('Company/Organization'),
|
|
303
|
+
title: z.string().optional().describe('Job title'),
|
|
304
|
+
notes: z.string().optional().describe('Notes about the contact')
|
|
305
|
+
},
|
|
306
|
+
annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false},
|
|
307
|
+
handler: withErrorHandler(handleCreateContact, 'create-contact')
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: 'delete-contact',
|
|
311
|
+
title: 'Delete Contact',
|
|
312
|
+
description: 'Deletes a contact',
|
|
313
|
+
inputSchema: {
|
|
314
|
+
contactUrl: z.string().describe('URL or ID of the contact to delete (from list-contacts output)')
|
|
315
|
+
},
|
|
316
|
+
annotations: {"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},
|
|
317
|
+
handler: withErrorHandler(handleDeleteContact, 'delete-contact')
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'list-contact-accounts',
|
|
321
|
+
outputSchema: listOutput('Contact accounts'),
|
|
322
|
+
title: 'List Contact Accounts',
|
|
323
|
+
description: 'Lists all contact accounts (iCloud, Google, Exchange, etc.) - LOCAL mode only',
|
|
324
|
+
inputSchema: {},
|
|
325
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
|
|
326
|
+
handler: withErrorHandler(handleListContactAccounts, 'list-contact-accounts')
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: 'list-contact-groups',
|
|
330
|
+
outputSchema: listOutput('Contact groups'),
|
|
331
|
+
title: 'List Contact Groups',
|
|
332
|
+
description: 'Lists contact groups from all accounts or a specific account - LOCAL mode only',
|
|
333
|
+
inputSchema: {
|
|
334
|
+
accountId: z.string().optional().describe('Optional account ID to filter groups by (from list-contact-accounts)')
|
|
335
|
+
},
|
|
336
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
|
|
337
|
+
handler: withErrorHandler(handleListContactGroups, 'list-contact-groups')
|
|
338
|
+
}
|
|
339
|
+
];
|
|
340
|
+
|
|
341
|
+
module.exports = {
|
|
342
|
+
contactsTools,
|
|
343
|
+
handleListContacts,
|
|
344
|
+
handleSearchContacts,
|
|
345
|
+
handleReadContact,
|
|
346
|
+
handleCreateContact,
|
|
347
|
+
handleDeleteContact,
|
|
348
|
+
handleListContactAccounts,
|
|
349
|
+
handleListContactGroups
|
|
350
|
+
};
|