mcp-icloud 2.1.3 → 2.1.5
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 +1 -1
- package/email/index.js +2 -2
- package/email/local-client.js +44 -17
- package/messages/index.js +45 -15
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ Tools marked local only return an error in cloud mode, because the iCloud protoc
|
|
|
103
103
|
|
|
104
104
|
Reading requires the [`imsg`](https://github.com/steipete/imsg) CLI and Full Disk Access.
|
|
105
105
|
|
|
106
|
-
- **list-chats**: `limit` (number, optional)
|
|
106
|
+
- **list-chats**: `limit` (number, optional, max 1000), `since` (ISO 8601, optional — only chats active since that date, e.g. a weekly review)
|
|
107
107
|
- **read-messages**: `chatId` (number); `limit` (number, optional); `start`, `end` (ISO 8601, optional); `attachments` (boolean, optional)
|
|
108
108
|
- **send-message**: `to` (string), `body` (string), `file` (string, optional)
|
|
109
109
|
- **react-message**: `chatId` (number), `type` (`love`, `like`, `dislike`, `laugh`, `emphasis`, `question`)
|
package/email/index.js
CHANGED
|
@@ -64,7 +64,7 @@ async function handleReadEmail(args) {
|
|
|
64
64
|
const local = isLocalMode();
|
|
65
65
|
|
|
66
66
|
const email = local
|
|
67
|
-
? await localClient.readEmail(args.uid)
|
|
67
|
+
? await localClient.readEmail(args.uid, folder)
|
|
68
68
|
: await cloudClient.readEmail(args.uid, folder);
|
|
69
69
|
|
|
70
70
|
// Mail.app returns the body as `body`; mailparser returns `text`.
|
|
@@ -167,7 +167,7 @@ async function handleMarkAsRead(args) {
|
|
|
167
167
|
const isRead = args.isRead !== false;
|
|
168
168
|
|
|
169
169
|
if (isLocalMode()) {
|
|
170
|
-
await localClient.markAsRead(args.uid, isRead);
|
|
170
|
+
await localClient.markAsRead(args.uid, isRead, folder);
|
|
171
171
|
} else {
|
|
172
172
|
await cloudClient.markAsRead(args.uid, folder, isRead);
|
|
173
173
|
}
|
package/email/local-client.js
CHANGED
|
@@ -50,15 +50,42 @@ async function listEmails(folder = 'inbox', count = 25) {
|
|
|
50
50
|
return result ? JSON.parse(result) : [];
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* JXA statements that set `mail` and `msg` to message `emailId`.
|
|
55
|
+
* Mail has no application-level messages collection, so
|
|
56
|
+
* `mail.messages.byId(id)` fails with -1728 (issue #15). A mailbox's
|
|
57
|
+
* `messages.byId(id)` works; the named folder is tried first.
|
|
58
|
+
* @param {string|number} emailId - Email ID
|
|
59
|
+
* @param {string} folder - Folder name
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function findMessageJXA(emailId, folder) {
|
|
63
|
+
const id = asInt(emailId);
|
|
64
|
+
return `
|
|
65
|
+
const mail = Application('Mail');
|
|
66
|
+
let msg = null;
|
|
67
|
+
for (const account of mail.accounts()) {
|
|
68
|
+
const mailboxes = [account.mailboxes.byName('${escapeJXA(getMailboxName(folder || 'inbox'))}')].concat(account.mailboxes());
|
|
69
|
+
for (const mailbox of mailboxes) {
|
|
70
|
+
if (!mailbox.exists()) continue;
|
|
71
|
+
const candidate = mailbox.messages.byId(${id});
|
|
72
|
+
if (candidate.exists()) { msg = candidate; break; }
|
|
73
|
+
}
|
|
74
|
+
if (msg) break;
|
|
75
|
+
}
|
|
76
|
+
if (!msg) throw new Error('Email not found: ${id}');
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
53
80
|
/**
|
|
54
81
|
* Read a specific email
|
|
55
82
|
* @param {string} emailId - Email ID
|
|
83
|
+
* @param {string} [folder] - Folder to look in first
|
|
56
84
|
* @returns {Promise<Object>} - Email content
|
|
57
85
|
*/
|
|
58
|
-
async function readEmail(emailId) {
|
|
86
|
+
async function readEmail(emailId, folder = 'inbox') {
|
|
59
87
|
const script = `
|
|
60
|
-
|
|
61
|
-
const msg = mail.messages.byId(${asInt(emailId)});
|
|
88
|
+
${findMessageJXA(emailId, folder)}
|
|
62
89
|
|
|
63
90
|
JSON.stringify({
|
|
64
91
|
id: msg.id(),
|
|
@@ -170,18 +197,18 @@ async function searchEmails({ query, from, subject, folder = 'inbox', count = 25
|
|
|
170
197
|
* Mark email as read/unread
|
|
171
198
|
* @param {string} emailId - Email ID
|
|
172
199
|
* @param {boolean} isRead - Read status
|
|
200
|
+
* @param {string} [folder] - Folder to look in first
|
|
173
201
|
* @returns {Promise<Object>} - Result
|
|
174
202
|
*/
|
|
175
|
-
async function markAsRead(emailId, isRead = true) {
|
|
203
|
+
async function markAsRead(emailId, isRead = true, folder = 'inbox') {
|
|
176
204
|
const script = `
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return "done"
|
|
205
|
+
${findMessageJXA(emailId, folder)}
|
|
206
|
+
msg = msg.mailbox().messages.byId(${asInt(emailId)});
|
|
207
|
+
msg.readStatus = ${asBool(isRead, true)};
|
|
208
|
+
'done';
|
|
182
209
|
`;
|
|
183
210
|
|
|
184
|
-
await
|
|
211
|
+
await runJXA(script);
|
|
185
212
|
return { success: true, message: `Email marked as ${isRead ? 'read' : 'unread'}` };
|
|
186
213
|
}
|
|
187
214
|
|
|
@@ -216,18 +243,18 @@ async function listFolders() {
|
|
|
216
243
|
/**
|
|
217
244
|
* Delete an email
|
|
218
245
|
* @param {string} emailId - Email ID
|
|
246
|
+
* @param {string} [folder] - Folder to look in first
|
|
219
247
|
* @returns {Promise<Object>} - Result
|
|
220
248
|
*/
|
|
221
|
-
async function deleteEmail(emailId) {
|
|
249
|
+
async function deleteEmail(emailId, folder = 'inbox') {
|
|
222
250
|
const script = `
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return "deleted"
|
|
251
|
+
${findMessageJXA(emailId, folder)}
|
|
252
|
+
msg = msg.mailbox().messages.byId(${asInt(emailId)});
|
|
253
|
+
mail.delete(msg);
|
|
254
|
+
'deleted';
|
|
228
255
|
`;
|
|
229
256
|
|
|
230
|
-
await
|
|
257
|
+
await runJXA(script);
|
|
231
258
|
return { success: true, message: 'Email deleted' };
|
|
232
259
|
}
|
|
233
260
|
|
package/messages/index.js
CHANGED
|
@@ -9,7 +9,7 @@ const localClient = require('./local-client');
|
|
|
9
9
|
const { formatError, formatSuccess, withErrorHandler } = require('../utils/error-handler');
|
|
10
10
|
const { listOutput, listResult } = require('../utils/schemas');
|
|
11
11
|
const { isLocalMode } = require('../mode');
|
|
12
|
-
const {
|
|
12
|
+
const { execFile } = require('child_process');
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
|
|
15
15
|
// Homebrew's prefix differs between Apple Silicon and Intel, and an MCP server
|
|
@@ -29,6 +29,9 @@ function resolveImsg() {
|
|
|
29
29
|
return 'imsg';
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// Chats scanned when `since` is set; listing 5,000 takes well under a second.
|
|
33
|
+
const SINCE_FETCH_LIMIT = 5000;
|
|
34
|
+
|
|
32
35
|
function requireLocalMode(toolName) {
|
|
33
36
|
if (!isLocalMode()) {
|
|
34
37
|
return formatError(new Error(`${toolName} is only available in LOCAL mode. Use set-mode to switch.`));
|
|
@@ -36,14 +39,29 @@ function requireLocalMode(toolName) {
|
|
|
36
39
|
return null;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// Async on purpose: a synchronous call blocks the whole server, so parallel
|
|
43
|
+
// reads from a client queue behind each other until they time out (#17).
|
|
44
|
+
function execImsg(args) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
execFile(resolveImsg(), args, {
|
|
47
|
+
timeout: 30000,
|
|
44
48
|
encoding: 'utf-8',
|
|
45
|
-
|
|
49
|
+
maxBuffer: 64 * 1024 * 1024
|
|
50
|
+
}, (error, stdout, stderr) => {
|
|
51
|
+
if (error) {
|
|
52
|
+
error.stderr = stderr;
|
|
53
|
+
reject(error);
|
|
54
|
+
} else {
|
|
55
|
+
resolve(stdout);
|
|
56
|
+
}
|
|
46
57
|
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function runImsg(args) {
|
|
62
|
+
let output;
|
|
63
|
+
try {
|
|
64
|
+
output = await execImsg(args);
|
|
47
65
|
} catch (error) {
|
|
48
66
|
const detail = (error.stderr || error.message || '').toString();
|
|
49
67
|
if (detail.includes('permissionDenied')) {
|
|
@@ -52,6 +70,9 @@ function runImsg(args) {
|
|
|
52
70
|
if (error.code === 'ENOENT') {
|
|
53
71
|
throw new Error('The imsg CLI was not found. Install it (brew install imsg) or set ICLOUD_MCP_IMSG_PATH.');
|
|
54
72
|
}
|
|
73
|
+
if (error.killed) {
|
|
74
|
+
throw new Error('imsg timed out after 30s');
|
|
75
|
+
}
|
|
55
76
|
throw new Error(detail.trim() || 'imsg failed');
|
|
56
77
|
}
|
|
57
78
|
|
|
@@ -70,17 +91,26 @@ const messagesTools = [
|
|
|
70
91
|
name: 'list-chats',
|
|
71
92
|
outputSchema: listOutput('Recent conversations'),
|
|
72
93
|
title: 'List Chats',
|
|
73
|
-
description: 'List recent iMessage/SMS conversations
|
|
94
|
+
description: 'List recent iMessage/SMS conversations, most recent first. Use `since` to get only the conversations active after a date (e.g. the last 7 days).',
|
|
74
95
|
inputSchema: {
|
|
75
|
-
limit: z.number().int().min(1).max(
|
|
96
|
+
limit: z.number().int().min(1).max(1000).optional().describe('Maximum number of conversations to return (default 20)'),
|
|
97
|
+
since: z.string().refine(v => !Number.isNaN(Date.parse(v)), 'since must be an ISO 8601 date')
|
|
98
|
+
.optional().describe('Only conversations with a message at or after this date (ISO 8601, optional)')
|
|
76
99
|
},
|
|
77
100
|
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
|
|
78
|
-
handler: withErrorHandler(async ({ limit = 20 }) => {
|
|
101
|
+
handler: withErrorHandler(async ({ limit = 20, since }) => {
|
|
79
102
|
const modeError = requireLocalMode('list-chats');
|
|
80
103
|
if (modeError) return modeError;
|
|
81
|
-
|
|
104
|
+
// imsg has no date filter, but returns chats newest first, so fetch a
|
|
105
|
+
// wide window and cut it at `since` here.
|
|
106
|
+
const fetchLimit = since ? Math.max(limit, SINCE_FETCH_LIMIT) : limit;
|
|
107
|
+
const result = await runImsg(['chats', '--limit', String(fetchLimit), '--json']);
|
|
82
108
|
// imsg emits JSONL, and runImsg unwraps a single line to a bare object.
|
|
83
|
-
|
|
109
|
+
let chats = Array.isArray(result) ? result : (result ? [result] : []);
|
|
110
|
+
if (since) {
|
|
111
|
+
const cutoff = Date.parse(since);
|
|
112
|
+
chats = chats.filter(c => Date.parse(c.last_message_at) >= cutoff).slice(0, limit);
|
|
113
|
+
}
|
|
84
114
|
return formatSuccess(JSON.stringify(chats, null, 2), listResult(chats));
|
|
85
115
|
}, 'list-chats')
|
|
86
116
|
},
|
|
@@ -103,7 +133,7 @@ const messagesTools = [
|
|
|
103
133
|
if (start) args.push('--start', start);
|
|
104
134
|
if (end) args.push('--end', end);
|
|
105
135
|
if (attachments) args.push('--attachments');
|
|
106
|
-
const result = runImsg(args);
|
|
136
|
+
const result = await runImsg(args);
|
|
107
137
|
return formatSuccess(JSON.stringify(result, null, 2));
|
|
108
138
|
}, 'read-messages')
|
|
109
139
|
},
|
|
@@ -128,7 +158,7 @@ const messagesTools = [
|
|
|
128
158
|
try {
|
|
129
159
|
const args = ['send', '--to', to, '--text', body];
|
|
130
160
|
if (file) args.push('--file', file);
|
|
131
|
-
const result = runImsg(args);
|
|
161
|
+
const result = await runImsg(args);
|
|
132
162
|
return formatSuccess(JSON.stringify(result, null, 2));
|
|
133
163
|
} catch (e) {
|
|
134
164
|
throw new Error(`${error.message} (imsg fallback also failed: ${e.message})`);
|
|
@@ -148,7 +178,7 @@ const messagesTools = [
|
|
|
148
178
|
handler: withErrorHandler(async ({ chatId, type }) => {
|
|
149
179
|
const modeError = requireLocalMode('react-message');
|
|
150
180
|
if (modeError) return modeError;
|
|
151
|
-
const result = runImsg(['react', '--chat-id', String(chatId), '--type', type]);
|
|
181
|
+
const result = await runImsg(['react', '--chat-id', String(chatId), '--type', type]);
|
|
152
182
|
return formatSuccess(JSON.stringify(result, null, 2));
|
|
153
183
|
}, 'react-message')
|
|
154
184
|
}
|
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.5",
|
|
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": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
29
|
"start": "node index.js",
|
|
30
|
-
"test": "node test/security.test.js && node test/local-output.test.js",
|
|
30
|
+
"test": "node test/security.test.js && node test/local-output.test.js && node test/email-read.test.js && node test/messages.test.js",
|
|
31
31
|
"test-mode": "USE_TEST_MODE=true node index.js",
|
|
32
32
|
"inspect": "npx @modelcontextprotocol/inspector node index.js",
|
|
33
33
|
"test:local": "node test/local-output.test.js"
|