mcp-icloud 2.1.1 → 2.1.3
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/PRIVACY.md +55 -0
- package/README.md +24 -1
- package/calendar/index.js +2 -2
- package/calendar/local-client.js +17 -24
- package/contacts/index.js +3 -3
- package/email/index.js +3 -3
- package/index.js +8 -3
- package/messages/index.js +3 -3
- package/package.json +2 -1
- package/reminders/index.js +1 -1
- package/reminders/local-client.js +9 -3
- package/utils/applescript.js +18 -12
package/PRIVACY.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Privacy policy
|
|
2
|
+
|
|
3
|
+
Effective 10 August 2026. Applies to the mcp-icloud server (the software you
|
|
4
|
+
run), not to any AI client you connect it to.
|
|
5
|
+
|
|
6
|
+
## The short version
|
|
7
|
+
|
|
8
|
+
mcp-icloud runs on your machine and works for you. It has no servers of its
|
|
9
|
+
own, no analytics, no telemetry, and no way to see your data. The author
|
|
10
|
+
never receives anything.
|
|
11
|
+
|
|
12
|
+
## What the software touches, and where it goes
|
|
13
|
+
|
|
14
|
+
When a connected AI client (Claude Desktop, for example) calls a tool, the
|
|
15
|
+
server reads or writes data in your Apple services: mail, calendar events,
|
|
16
|
+
contacts, reminders, notes, messages, and Safari tabs. Two things happen
|
|
17
|
+
with that data, and nothing else.
|
|
18
|
+
|
|
19
|
+
First, tool results are returned to the AI client that asked for them. What
|
|
20
|
+
that client does with the data is governed by its own privacy policy, not
|
|
21
|
+
this one. Nothing is sent anywhere unless a client you connected asks.
|
|
22
|
+
|
|
23
|
+
Second, in cloud mode the server talks to Apple's servers on your behalf
|
|
24
|
+
(imap.mail.me.com, smtp.mail.me.com, caldav.icloud.com,
|
|
25
|
+
contacts.icloud.com), over TLS, authenticated with credentials you
|
|
26
|
+
provided. In local mode the server makes no network connections at all; it
|
|
27
|
+
drives the apps already on your Mac, and macOS asks for your consent per
|
|
28
|
+
app before any of that works. You can revoke that consent at any time in
|
|
29
|
+
System Settings under Privacy and Security.
|
|
30
|
+
|
|
31
|
+
## Credentials
|
|
32
|
+
|
|
33
|
+
Cloud mode needs your iCloud address and an app-specific password. You
|
|
34
|
+
store them yourself, either in a local .env file or in the macOS Keychain
|
|
35
|
+
when you install the desktop extension. The server reads them once at
|
|
36
|
+
startup, sends them only to Apple, and never writes them to any log.
|
|
37
|
+
Diagnostics print a fixed mask instead of the value. Local mode needs no
|
|
38
|
+
credentials at all.
|
|
39
|
+
|
|
40
|
+
## What is kept
|
|
41
|
+
|
|
42
|
+
Nothing. The server is stateless: no database, no cache, no files written.
|
|
43
|
+
When it stops, nothing of yours remains in it.
|
|
44
|
+
|
|
45
|
+
## Third parties
|
|
46
|
+
|
|
47
|
+
There are none. No data is sold, shared, or transferred to anyone. The only
|
|
48
|
+
parties that ever see your data are Apple (which already has it) and the AI
|
|
49
|
+
client you chose to connect.
|
|
50
|
+
|
|
51
|
+
## Changes and contact
|
|
52
|
+
|
|
53
|
+
Changes to this policy are made in this file, in the open, with the
|
|
54
|
+
repository's history as the record. Questions and concerns:
|
|
55
|
+
https://github.com/MrGo2/icloud-mcp/issues
|
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,10 @@ 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
|
+
## Privacy Policy
|
|
38
|
+
|
|
39
|
+
The server collects nothing and keeps nothing: no telemetry, no analytics, no state on disk. Your data moves only between your machine, the AI client you connect and Apple's services. The full policy, covering collection, storage, third-party sharing, retention and contact, is in [PRIVACY.md](PRIVACY.md).
|
|
40
|
+
|
|
32
41
|
## Tools
|
|
33
42
|
|
|
34
43
|
Tools marked local only return an error in cloud mode, because the iCloud protocols do not expose those services.
|
|
@@ -108,12 +117,26 @@ Reading requires the [`imsg`](https://github.com/steipete/imsg) CLI and Full Dis
|
|
|
108
117
|
|
|
109
118
|
### Tool annotations (MCP hints)
|
|
110
119
|
|
|
111
|
-
Every tool declares its behaviour explicitly instead of relying on the spec defaults, which are deliberately pessimistic. All `list-*`, `read-*`, `search-*` and `get-*` tools, plus `about` and `check-auth-status`, are marked read-only.
|
|
120
|
+
Every tool declares its behaviour explicitly instead of relying on the spec defaults, which are deliberately pessimistic. All `list-*`, `read-*`, `search-*` and `get-*` tools, plus `about` and `check-auth-status`, are marked read-only. Nine tools carry `destructiveHint`: the four deletions (`delete-event`, `delete-contact`, `delete-reminder`, `close-safari-tab`), the three that reach another person irrevocably (`send-email`, `send-message`, `react-message`) and the two in-place updates (`update-event`, `update-reminder`). Tools that reach the network or another person carry `openWorldHint`: all Email and Calendar tools, `send-message`, `react-message` and `open-safari-url`. The read-only tools plus `mark-as-read`, `complete-reminder`, `update-reminder`, `update-event` and `set-mode` are marked idempotent.
|
|
112
121
|
|
|
113
122
|
## Installation
|
|
114
123
|
|
|
115
124
|
Requires Node.js 20 or newer.
|
|
116
125
|
|
|
126
|
+
### Let your AI install it
|
|
127
|
+
|
|
128
|
+
Paste this into Claude, Claude Code, Cursor, ChatGPT or any agent with shell or config access:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Install the iCloud MCP server. It is the npm package "mcp-icloud" (Node 20+),
|
|
132
|
+
stdio transport, server name "icloud", command "npx" with args ["-y", "mcp-icloud"].
|
|
133
|
+
On Claude Code run: claude mcp add --transport stdio icloud -- npx -y mcp-icloud
|
|
134
|
+
For other clients, add that command/args pair to the client's MCP config file.
|
|
135
|
+
Local mode (default) needs macOS; cloud mode needs env vars USE_LOCAL_MODE=false,
|
|
136
|
+
ICLOUD_EMAIL and ICLOUD_APP_PASSWORD (app-specific password from account.apple.com).
|
|
137
|
+
Docs: https://github.com/MrGo2/icloud-mcp
|
|
138
|
+
```
|
|
139
|
+
|
|
117
140
|
### Claude Desktop
|
|
118
141
|
|
|
119
142
|
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)')
|
|
@@ -218,7 +218,7 @@ const calendarTools = [
|
|
|
218
218
|
description: z.string().optional().describe('New description (optional)'),
|
|
219
219
|
location: z.string().optional().describe('New location (optional)')
|
|
220
220
|
},
|
|
221
|
-
annotations: {"readOnlyHint":false,"destructiveHint":
|
|
221
|
+
annotations: {"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":true},
|
|
222
222
|
handler: withErrorHandler(handleUpdateEvent, 'update-event')
|
|
223
223
|
},
|
|
224
224
|
{
|
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; 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'),
|
|
@@ -225,14 +225,14 @@ const emailTools = [
|
|
|
225
225
|
body: z.string().describe('Email body content'),
|
|
226
226
|
isHtml: z.boolean().optional().describe('Whether the body is HTML (default: false)')
|
|
227
227
|
},
|
|
228
|
-
annotations: {"readOnlyHint":false,"destructiveHint":
|
|
228
|
+
annotations: {"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true},
|
|
229
229
|
handler: withErrorHandler(handleSendEmail, 'send-email')
|
|
230
230
|
},
|
|
231
231
|
{
|
|
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* iCloud MCP Server
|
|
4
|
+
* iCloud MCP Server
|
|
5
5
|
*
|
|
6
6
|
* Provides Claude with access to Apple services:
|
|
7
7
|
* - Email (via IMAP/SMTP or Mail.app)
|
|
@@ -21,6 +21,7 @@ const { McpServer } = require('@modelcontextprotocol/server');
|
|
|
21
21
|
const { serveStdio } = require('@modelcontextprotocol/server/stdio');
|
|
22
22
|
|
|
23
23
|
const config = require('./config');
|
|
24
|
+
const { version } = require('./package.json');
|
|
24
25
|
const { getMode, onModeChange } = require('./mode');
|
|
25
26
|
|
|
26
27
|
// Import all modules - tools are always available, handlers check mode
|
|
@@ -56,11 +57,15 @@ function buildServer() {
|
|
|
56
57
|
const server = new McpServer(
|
|
57
58
|
{
|
|
58
59
|
name: 'icloud-mcp',
|
|
59
|
-
version
|
|
60
|
+
version,
|
|
60
61
|
title: 'iCloud MCP'
|
|
61
62
|
},
|
|
62
63
|
{
|
|
63
64
|
capabilities: { tools: { listChanged: true } },
|
|
65
|
+
// The 41-tool list is identical in both modes (mode changes behaviour,
|
|
66
|
+
// not the roster), so clients may cache it. Without this the SDK
|
|
67
|
+
// advertises ttlMs: 0 and every 2026-era client re-fetches constantly.
|
|
68
|
+
cacheHints: { 'tools/list': { ttlMs: 3600000, cacheScope: 'public' } },
|
|
64
69
|
instructions: `Access to Apple services in two modes.
|
|
65
70
|
|
|
66
71
|
LOCAL (default, macOS only) drives the native apps via AppleScript.
|
|
@@ -94,7 +99,7 @@ mode returns an error. Use set-mode to switch without restarting.`
|
|
|
94
99
|
return server;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
console.error(
|
|
102
|
+
console.error(`[icloud-mcp] Starting iCloud MCP server v${version}...`);
|
|
98
103
|
console.error(`[icloud-mcp] Initial mode: ${getMode().toUpperCase()}`);
|
|
99
104
|
console.error(`[icloud-mcp] Tools available: ${TOOLS.length}`);
|
|
100
105
|
|
package/messages/index.js
CHANGED
|
@@ -110,13 +110,13 @@ const messagesTools = [
|
|
|
110
110
|
{
|
|
111
111
|
name: 'send-message',
|
|
112
112
|
title: 'Send Message',
|
|
113
|
-
description: '
|
|
113
|
+
description: 'Sends an iMessage or SMS to a phone number or email handle, with an optional file attachment. The message is delivered as soon as the tool runs; there is no draft step and no recall.',
|
|
114
114
|
inputSchema: {
|
|
115
115
|
to: z.string().describe('Recipient phone number (with country code) or email'),
|
|
116
116
|
body: z.string().describe('Message content'),
|
|
117
117
|
file: z.string().optional().describe('Path to file attachment (optional)')
|
|
118
118
|
},
|
|
119
|
-
annotations: {"readOnlyHint":false,"destructiveHint":
|
|
119
|
+
annotations: {"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true},
|
|
120
120
|
handler: withErrorHandler(async ({ to, body, file }) => {
|
|
121
121
|
const modeError = requireLocalMode('send-message');
|
|
122
122
|
if (modeError) return modeError;
|
|
@@ -144,7 +144,7 @@ const messagesTools = [
|
|
|
144
144
|
chatId: z.number().int().describe('Chat ID'),
|
|
145
145
|
type: z.enum(['love', 'like', 'dislike', 'laugh', 'emphasis', 'question']).describe('Reaction type')
|
|
146
146
|
},
|
|
147
|
-
annotations: {"readOnlyHint":false,"destructiveHint":
|
|
147
|
+
annotations: {"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true},
|
|
148
148
|
handler: withErrorHandler(async ({ chatId, type }) => {
|
|
149
149
|
const modeError = requireLocalMode('react-message');
|
|
150
150
|
if (modeError) return modeError;
|
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.3",
|
|
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": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"utils/",
|
|
23
23
|
"LICENSE",
|
|
24
24
|
"README.md",
|
|
25
|
+
"PRIVACY.md",
|
|
25
26
|
"DISCLOSURE"
|
|
26
27
|
],
|
|
27
28
|
"scripts": {
|
package/reminders/index.js
CHANGED
|
@@ -115,7 +115,7 @@ const remindersTools = [
|
|
|
115
115
|
dueDate: z.string().optional().describe('New due date in ISO format'),
|
|
116
116
|
priority: z.number().int().min(0).max(9).optional().describe('New priority level')
|
|
117
117
|
},
|
|
118
|
-
annotations: {"readOnlyHint":false,"destructiveHint":
|
|
118
|
+
annotations: {"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},
|
|
119
119
|
handler: async ({ reminderId, ...updates }) => {
|
|
120
120
|
const modeError = requireLocalMode('update-reminder');
|
|
121
121
|
if (modeError) return modeError;
|
|
@@ -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
|
};
|