mcp-icloud 2.1.1 → 2.1.2
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 +21 -0
- package/calendar/index.js +1 -1
- package/calendar/local-client.js +17 -24
- package/contacts/index.js +3 -3
- package/email/index.js +2 -2
- package/index.js +4 -0
- package/package.json +1 -1
- package/reminders/local-client.js +9 -3
- package/utils/applescript.js +18 -12
package/README.md
CHANGED
|
@@ -5,17 +5,22 @@
|
|
|
5
5
|
|
|
6
6
|
<div align="center">
|
|
7
7
|
|
|
8
|
+
[](https://www.npmjs.com/package/mcp-icloud)
|
|
8
9
|
[](https://github.com/MrGo2/icloud-mcp/actions/workflows/ci.yml)
|
|
9
10
|
[](https://github.com/MrGo2/icloud-mcp/releases/latest)
|
|
10
11
|
[](LICENSE)
|
|
11
12
|
[](package.json)
|
|
12
13
|
[](https://modelcontextprotocol.io)
|
|
14
|
+
[](https://glama.ai/mcp/servers/MrGo2/icloud-mcp)
|
|
13
15
|
|
|
14
16
|
</div>
|
|
15
17
|
|
|
18
|
+
<img src="assets/demo.gif" alt="Claude creating a note, a reminder and a calendar event through icloud-mcp" width="100%">
|
|
19
|
+
|
|
16
20
|
icloud-mcp covers seven Apple services with two interchangeable backends, and as far as we know it is the only MCP server that does. In local mode it drives the native macOS apps through AppleScript: no credentials, no network, and it reaches the services the iCloud protocols do not expose (Reminders, Notes, Messages, Safari). In cloud mode it speaks IMAP/SMTP, CalDAV and CardDAV instead, so it runs on any machine, not just a Mac. The `set-mode` tool switches between the two at runtime, without a restart.
|
|
17
21
|
|
|
18
22
|
[](https://insiders.vscode.dev/redirect/mcp/install?name=icloud&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22mcp-icloud%22%5D%7D)
|
|
23
|
+
[](https://cursor.com/en/install-mcp?name=icloud&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1pY2xvdWQiXX0=)
|
|
19
24
|
|
|
20
25
|
## Features
|
|
21
26
|
|
|
@@ -29,6 +34,8 @@ Credentials are never logged. `ICLOUD_APP_PASSWORD` is read once at startup and
|
|
|
29
34
|
|
|
30
35
|
Every tool argument is validated against a zod schema before the handler runs. Arguments are also coerced at each AppleScript interpolation site, so a value that is not a number cannot reach a script template.
|
|
31
36
|
|
|
37
|
+
The server collects nothing and keeps nothing: no telemetry, no analytics, no state on disk. The full policy is in [PRIVACY.md](PRIVACY.md).
|
|
38
|
+
|
|
32
39
|
## Tools
|
|
33
40
|
|
|
34
41
|
Tools marked local only return an error in cloud mode, because the iCloud protocols do not expose those services.
|
|
@@ -114,6 +121,20 @@ Every tool declares its behaviour explicitly instead of relying on the spec defa
|
|
|
114
121
|
|
|
115
122
|
Requires Node.js 20 or newer.
|
|
116
123
|
|
|
124
|
+
### Let your AI install it
|
|
125
|
+
|
|
126
|
+
Paste this into Claude, Claude Code, Cursor, ChatGPT or any agent with shell or config access:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
Install the iCloud MCP server. It is the npm package "mcp-icloud" (Node 20+),
|
|
130
|
+
stdio transport, server name "icloud", command "npx" with args ["-y", "mcp-icloud"].
|
|
131
|
+
On Claude Code run: claude mcp add --transport stdio icloud -- npx -y mcp-icloud
|
|
132
|
+
For other clients, add that command/args pair to the client's MCP config file.
|
|
133
|
+
Local mode (default) needs macOS; cloud mode needs env vars USE_LOCAL_MODE=false,
|
|
134
|
+
ICLOUD_EMAIL and ICLOUD_APP_PASSWORD (app-specific password from account.apple.com).
|
|
135
|
+
Docs: https://github.com/MrGo2/icloud-mcp
|
|
136
|
+
```
|
|
137
|
+
|
|
117
138
|
### Claude Desktop
|
|
118
139
|
|
|
119
140
|
Add to `claude_desktop_config.json`:
|
package/calendar/index.js
CHANGED
|
@@ -182,7 +182,7 @@ const calendarTools = [
|
|
|
182
182
|
name: 'list-events',
|
|
183
183
|
outputSchema: listOutput('Calendar events'),
|
|
184
184
|
title: 'List Events',
|
|
185
|
-
description: 'Lists upcoming calendar events',
|
|
185
|
+
description: 'Lists upcoming calendar events within a look-ahead window (30 days by default, up to 365). Each event carries its title, start and end times, and the ref handle that update-event and delete-event take as their eventUrl argument.',
|
|
186
186
|
inputSchema: {
|
|
187
187
|
count: z.number().int().min(1).max(50).optional().describe('Number of events to retrieve (default: 25, max: 50)'),
|
|
188
188
|
daysAhead: z.number().int().min(1).max(365).optional().describe('Number of days to look ahead (default: 30)')
|
package/calendar/local-client.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Accesses Calendar.app via AppleScript
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const { runAppleScript, runJXA, escapeAppleScript, escapeJXA, asInt,
|
|
6
|
+
const { runAppleScript, runJXA, escapeAppleScript, escapeJXA, asInt, appleScriptDateStmts } = require('../utils/applescript');
|
|
7
7
|
const config = require('../config');
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -115,26 +115,12 @@ async function createEvent({ summary, start, end, location, description, calenda
|
|
|
115
115
|
// Use default calendar if none specified
|
|
116
116
|
const targetCalendar = calendarName || 'Calendar';
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
// All-day event
|
|
120
|
-
const script = `
|
|
121
|
-
tell application "Calendar"
|
|
122
|
-
tell calendar "${escapeAppleScript(targetCalendar)}"
|
|
123
|
-
set newEvent to make new event with properties {summary:"${escapeAppleScript(summary)}", start date:date "${formatAppleScriptDate(startDate)}", end date:date "${formatAppleScriptDate(endDate)}", allday event:true}
|
|
124
|
-
${location ? `set location of newEvent to "${escapeAppleScript(location)}"` : ''}
|
|
125
|
-
${description ? `set description of newEvent to "${escapeAppleScript(description)}"` : ''}
|
|
126
|
-
return uid of newEvent
|
|
127
|
-
end tell
|
|
128
|
-
end tell
|
|
129
|
-
`;
|
|
130
|
-
|
|
131
|
-
const uid = await runAppleScript(script);
|
|
132
|
-
return { success: true, id: uid, message: 'Event created successfully' };
|
|
133
|
-
} else {
|
|
134
|
-
const script = `
|
|
118
|
+
const script = `
|
|
135
119
|
tell application "Calendar"
|
|
120
|
+
${appleScriptDateStmts('newStart', startDate)}
|
|
121
|
+
${appleScriptDateStmts('newEnd', endDate)}
|
|
136
122
|
tell calendar "${escapeAppleScript(targetCalendar)}"
|
|
137
|
-
set newEvent to make new event with properties {summary:"${escapeAppleScript(summary)}", start date:date
|
|
123
|
+
set newEvent to make new event with properties {summary:"${escapeAppleScript(summary)}", start date:newStart, end date:newEnd${allDay ? ', allday event:true' : ''}}
|
|
138
124
|
${location ? `set location of newEvent to "${escapeAppleScript(location)}"` : ''}
|
|
139
125
|
${description ? `set description of newEvent to "${escapeAppleScript(description)}"` : ''}
|
|
140
126
|
return uid of newEvent
|
|
@@ -142,9 +128,8 @@ async function createEvent({ summary, start, end, location, description, calenda
|
|
|
142
128
|
end tell
|
|
143
129
|
`;
|
|
144
130
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
131
|
+
const uid = await runAppleScript(script);
|
|
132
|
+
return { success: true, id: uid, message: 'Event created successfully' };
|
|
148
133
|
}
|
|
149
134
|
|
|
150
135
|
/**
|
|
@@ -155,10 +140,17 @@ async function createEvent({ summary, start, end, location, description, calenda
|
|
|
155
140
|
*/
|
|
156
141
|
async function updateEvent(eventId, { summary, start, end, location, description }) {
|
|
157
142
|
let updateCommands = [];
|
|
143
|
+
let dateSetup = [];
|
|
158
144
|
|
|
159
145
|
if (summary) updateCommands.push(`set summary of theEvent to "${escapeAppleScript(summary)}"`);
|
|
160
|
-
if (start)
|
|
161
|
-
|
|
146
|
+
if (start) {
|
|
147
|
+
dateSetup.push(appleScriptDateStmts('newStart', new Date(start)));
|
|
148
|
+
updateCommands.push('set start date of theEvent to newStart');
|
|
149
|
+
}
|
|
150
|
+
if (end) {
|
|
151
|
+
dateSetup.push(appleScriptDateStmts('newEnd', new Date(end)));
|
|
152
|
+
updateCommands.push('set end date of theEvent to newEnd');
|
|
153
|
+
}
|
|
162
154
|
if (location !== undefined) updateCommands.push(`set location of theEvent to "${escapeAppleScript(location || '')}"`);
|
|
163
155
|
if (description !== undefined) updateCommands.push(`set description of theEvent to "${escapeAppleScript(description || '')}"`);
|
|
164
156
|
|
|
@@ -168,6 +160,7 @@ async function updateEvent(eventId, { summary, start, end, location, description
|
|
|
168
160
|
|
|
169
161
|
const script = `
|
|
170
162
|
tell application "Calendar"
|
|
163
|
+
${dateSetup.join('\n ')}
|
|
171
164
|
set allCalendars to calendars
|
|
172
165
|
repeat with cal in allCalendars
|
|
173
166
|
try
|
package/contacts/index.js
CHANGED
|
@@ -260,7 +260,7 @@ const contactsTools = [
|
|
|
260
260
|
name: 'list-contacts',
|
|
261
261
|
outputSchema: listOutput('Contacts'),
|
|
262
262
|
title: 'List Contacts',
|
|
263
|
-
description: 'Lists contacts from
|
|
263
|
+
description: 'Lists contacts from the user\'s address book, up to 50 per call, including the handle that read-contact and delete-contact take. For a targeted lookup, search-contacts filters by name, email or phone instead of paging through the full list.',
|
|
264
264
|
inputSchema: {
|
|
265
265
|
count: z.number().int().min(1).max(50).optional().describe('Number of contacts to retrieve (default: 25, max: 50)')
|
|
266
266
|
},
|
|
@@ -292,7 +292,7 @@ const contactsTools = [
|
|
|
292
292
|
{
|
|
293
293
|
name: 'create-contact',
|
|
294
294
|
title: 'Create Contact',
|
|
295
|
-
description: 'Creates a
|
|
295
|
+
description: 'Creates a contact in the user\'s address book (Contacts.app in local mode, iCloud over CardDAV in cloud mode). Every field is optional, so pass at least a name (displayName, or firstName and lastName) along with any known email, phone, organization, job title or notes.',
|
|
296
296
|
inputSchema: {
|
|
297
297
|
displayName: z.string().optional().describe('Full display name'),
|
|
298
298
|
firstName: z.string().optional().describe('First name'),
|
|
@@ -309,7 +309,7 @@ const contactsTools = [
|
|
|
309
309
|
{
|
|
310
310
|
name: 'delete-contact',
|
|
311
311
|
title: 'Delete Contact',
|
|
312
|
-
description: '
|
|
312
|
+
description: 'Permanently deletes a contact from the user\'s address book. Takes the handle returned by list-contacts or search-contacts; confirm the target with read-contact first, because there is no undo through this server.',
|
|
313
313
|
inputSchema: {
|
|
314
314
|
contactUrl: z.string().describe('URL or ID of the contact to delete (from list-contacts output)')
|
|
315
315
|
},
|
package/email/index.js
CHANGED
|
@@ -216,7 +216,7 @@ const emailTools = [
|
|
|
216
216
|
{
|
|
217
217
|
name: 'send-email',
|
|
218
218
|
title: 'Send Email',
|
|
219
|
-
description: '
|
|
219
|
+
description: 'Sends an email immediately from the user\'s account to one or more comma-separated recipients, with optional CC, BCC and HTML body. Cloud mode delivers over SMTP and returns the new message ID; local mode sends through Mail.app, which only sends plain text and returns no ID. There is no draft step, so the message goes out as soon as the tool runs.',
|
|
220
220
|
inputSchema: {
|
|
221
221
|
to: z.string().describe('Recipient email address(es), comma-separated'),
|
|
222
222
|
cc: z.string().optional().describe('CC recipient(s), comma-separated'),
|
|
@@ -232,7 +232,7 @@ const emailTools = [
|
|
|
232
232
|
name: 'search-emails',
|
|
233
233
|
outputSchema: listOutput('Matching emails'),
|
|
234
234
|
title: 'Search Emails',
|
|
235
|
-
description: '
|
|
235
|
+
description: 'Searches one mail folder (inbox by default) by free text, sender, subject and unread state; the filters combine as AND. Results carry the same summary fields as list-emails, including the ref handle that read-email and mark-as-read take.',
|
|
236
236
|
inputSchema: {
|
|
237
237
|
query: z.string().optional().describe('Text to search in email content'),
|
|
238
238
|
from: z.string().optional().describe('Filter by sender'),
|
package/index.js
CHANGED
|
@@ -61,6 +61,10 @@ function buildServer() {
|
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
capabilities: { tools: { listChanged: true } },
|
|
64
|
+
// The 41-tool list is identical in both modes (mode changes behaviour,
|
|
65
|
+
// not the roster), so clients may cache it. Without this the SDK
|
|
66
|
+
// advertises ttlMs: 0 and every 2026-era client re-fetches constantly.
|
|
67
|
+
cacheHints: { 'tools/list': { ttlMs: 3600000, cacheScope: 'public' } },
|
|
64
68
|
instructions: `Access to Apple services in two modes.
|
|
65
69
|
|
|
66
70
|
LOCAL (default, macOS only) drives the native apps via AppleScript.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-icloud",
|
|
3
3
|
"mcpName": "io.github.MrGo2/icloud-mcp",
|
|
4
|
-
"version": "2.1.
|
|
4
|
+
"version": "2.1.2",
|
|
5
5
|
"description": "MCP server for Apple services - Email, Calendar, Contacts, Reminders, Notes, Messages and Safari - with dual local (AppleScript) and cloud (IMAP/CalDAV/CardDAV) modes",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Accesses Reminders.app via AppleScript
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const { runAppleScript, runJXA, escapeAppleScript, escapeJXA, asInt, asBool,
|
|
6
|
+
const { runAppleScript, runJXA, escapeAppleScript, escapeJXA, asInt, asBool, appleScriptDateStmts } = require('../utils/applescript');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* List all reminder lists
|
|
@@ -109,7 +109,8 @@ async function createReminder({ name, body, dueDate, listName = 'Reminders', pri
|
|
|
109
109
|
|
|
110
110
|
if (dueDate) {
|
|
111
111
|
script += `
|
|
112
|
-
|
|
112
|
+
${appleScriptDateStmts('theDue', new Date(dueDate))}
|
|
113
|
+
set due date of newReminder to theDue
|
|
113
114
|
`;
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -135,11 +136,16 @@ async function updateReminder(reminderId, { name, body, dueDate, priority }) {
|
|
|
135
136
|
if (name) updateCommands.push(`set name of theReminder to "${escapeAppleScript(name)}"`);
|
|
136
137
|
if (body !== undefined) updateCommands.push(`set body of theReminder to "${escapeAppleScript(body || '')}"`);
|
|
137
138
|
if (priority !== undefined) updateCommands.push(`set priority of theReminder to ${asInt(priority, 0)}`);
|
|
138
|
-
|
|
139
|
+
let dateSetup = [];
|
|
140
|
+
if (dueDate) {
|
|
141
|
+
dateSetup.push(appleScriptDateStmts('theDue', new Date(dueDate)));
|
|
142
|
+
updateCommands.push('set due date of theReminder to theDue');
|
|
143
|
+
}
|
|
139
144
|
if (dueDate === null) updateCommands.push(`set due date of theReminder to missing value`);
|
|
140
145
|
|
|
141
146
|
const script = `
|
|
142
147
|
tell application "Reminders"
|
|
148
|
+
${dateSetup.join('\n ')}
|
|
143
149
|
set allLists to lists
|
|
144
150
|
repeat with theList in allLists
|
|
145
151
|
try
|
package/utils/applescript.js
CHANGED
|
@@ -161,18 +161,24 @@ function asBool(value, fallback = false) {
|
|
|
161
161
|
* @param {Date|string} date - Date to format
|
|
162
162
|
* @returns {string} - AppleScript date string
|
|
163
163
|
*/
|
|
164
|
-
|
|
164
|
+
/**
|
|
165
|
+
* AppleScript statements that build a Date into `varName`.
|
|
166
|
+
* `date "August 15, 2026 6:00 PM"` literals only parse when the string matches
|
|
167
|
+
* the system locale, so the date is assembled numerically instead. Day is set
|
|
168
|
+
* to 1 first so applying the components one by one cannot overflow a month.
|
|
169
|
+
*/
|
|
170
|
+
function appleScriptDateStmts(varName, date) {
|
|
165
171
|
const d = date instanceof Date ? date : new Date(date);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
day
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${date}`);
|
|
173
|
+
const secs = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
|
|
174
|
+
return [
|
|
175
|
+
`set ${varName} to (current date)`,
|
|
176
|
+
`set day of ${varName} to 1`,
|
|
177
|
+
`set year of ${varName} to ${d.getFullYear()}`,
|
|
178
|
+
`set month of ${varName} to ${d.getMonth() + 1}`,
|
|
179
|
+
`set day of ${varName} to ${d.getDate()}`,
|
|
180
|
+
`set time of ${varName} to ${secs}`
|
|
181
|
+
].join('\n ');
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
/**
|
|
@@ -209,6 +215,6 @@ module.exports = {
|
|
|
209
215
|
escapeJXA,
|
|
210
216
|
asInt,
|
|
211
217
|
asBool,
|
|
212
|
-
|
|
218
|
+
appleScriptDateStmts,
|
|
213
219
|
AppleScriptError
|
|
214
220
|
};
|