icloud-mcp 1.4.0 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +45 -10
  2. package/index.js +144 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,6 +13,7 @@ A Model Context Protocol (MCP) server that connects Claude Desktop to your iClou
13
13
  - āœ… Mark emails as read/unread, flag/unflag in bulk or individually
14
14
  - šŸ—‚ļø List, create, rename, and delete mailboxes
15
15
  - šŸ”„ Dry run mode for bulk operations — preview before committing
16
+ - šŸ” Safe move — emails are fingerprinted and verified in the destination before being removed from the source
16
17
  - šŸ“ Session logging — Claude tracks progress across long multi-step operations
17
18
 
18
19
  ## Prerequisites
@@ -42,9 +43,39 @@ Then find the install location:
42
43
  npm root -g
43
44
  ```
44
45
 
45
- This will return a path like `/opt/homebrew/lib/node_modules` or `/usr/local/lib/node_modules`.
46
+ The path varies by setup:
46
47
 
47
- ### 3. Configure Claude Desktop
48
+ | Setup | Typical path |
49
+ |-------|-------------|
50
+ | Mac with Homebrew Node | `/opt/homebrew/lib/node_modules` |
51
+ | Mac with system Node | `/usr/local/lib/node_modules` |
52
+ | nvm | `~/.nvm/versions/node/v20.x.x/lib/node_modules` |
53
+
54
+ ### 3. Verify your setup
55
+
56
+ Before configuring Claude Desktop, run the doctor command to confirm everything is working:
57
+
58
+ ```bash
59
+ IMAP_USER="you@icloud.com" IMAP_PASSWORD="your-app-specific-password" node $(npm root -g)/icloud-mcp/index.js --doctor
60
+ ```
61
+
62
+ You should see:
63
+
64
+ ```
65
+ icloud-mcp doctor
66
+ ─────────────────────────────
67
+ āœ… IMAP_USER is set
68
+ āœ… IMAP_PASSWORD is set
69
+ āœ… Connected to imap.mail.me.com:993
70
+ āœ… Authenticated as you@icloud.com
71
+ āœ… INBOX opened (12453 messages)
72
+ ─────────────────────────────
73
+ All checks passed. Ready to use with Claude Desktop.
74
+ ```
75
+
76
+ If any step fails, a plain-English explanation and suggested fix will be shown.
77
+
78
+ ### 4. Configure Claude Desktop
48
79
 
49
80
  Open your Claude Desktop config file:
50
81
 
@@ -52,7 +83,7 @@ Open your Claude Desktop config file:
52
83
  open ~/Library/Application\ Support/Claude/claude_desktop_config.json
53
84
  ```
54
85
 
55
- Add the following under `mcpServers`, replacing the path with your npm root path from the previous step:
86
+ Add the following under `mcpServers`, replacing the path with your npm root from step 2:
56
87
 
57
88
  ```json
58
89
  {
@@ -69,9 +100,7 @@ Add the following under `mcpServers`, replacing the path with your npm root path
69
100
  }
70
101
  ```
71
102
 
72
- > **Note:** If your `npm root -g` returned a different path, replace `/opt/homebrew/lib/node_modules` with that path.
73
-
74
- ### 4. Add Custom Instructions (Recommended)
103
+ ### 5. Add Custom Instructions (Recommended)
75
104
 
76
105
  For large inbox operations, add the following to Claude Desktop's custom instructions to ensure Claude stays on track and checks in with you regularly. Go to **Claude Desktop → Settings → Custom Instructions** and add:
77
106
 
@@ -84,7 +113,7 @@ When using icloud-mail tools:
84
113
  5. If you are ever unsure what you have done so far, call log_read before proceeding
85
114
  ```
86
115
 
87
- ### 5. Restart Claude Desktop
116
+ ### 6. Restart Claude Desktop
88
117
 
89
118
  Fully quit Claude Desktop (Cmd+Q) and reopen it. You should now be able to manage your iCloud inbox through Claude.
90
119
 
@@ -120,6 +149,8 @@ Fully quit Claude Desktop (Cmd+Q) and reopen it. You should now be able to manag
120
149
  | `rename_mailbox` | Rename an existing folder |
121
150
  | `delete_mailbox` | Delete a folder (must be empty first) |
122
151
  | `empty_trash` | Permanently delete all emails in Deleted Messages |
152
+ | `get_move_status` | Check the status of the current or most recent bulk move operation |
153
+ | `abandon_move` | Abandon an in-progress move operation so a new one can start |
123
154
  | `log_write` | Write a step to the session log |
124
155
  | `log_read` | Read the session log to see what has been done so far |
125
156
  | `log_clear` | Clear the session log and start fresh |
@@ -141,13 +172,17 @@ Fully quit Claude Desktop (Cmd+Q) and reopen it. You should now be able to manag
141
172
  | `smaller` | number | Only emails smaller than this size in KB |
142
173
  | `hasAttachment` | boolean | Only emails with attachments |
143
174
 
144
- ### Dry Run Mode
175
+ ## Dry Run Mode
145
176
 
146
177
  Pass `dryRun: true` to `bulk_move` or `bulk_delete` to preview how many emails would be affected without making any changes:
147
178
 
148
179
  > *"How many emails would be deleted if I removed everything from linkedin.com before 2022?"*
149
180
 
150
- ### Session Log
181
+ ## Safe Move
182
+
183
+ All bulk move operations use a copy-verify-delete approach. Emails are fingerprinted before copying, confirmed present in the destination, and only then removed from the source. A persistent manifest at `~/.icloud-mcp-move-manifest.json` tracks progress across chunks so that a crash or connection drop mid-operation never results in data loss. Use `get_move_status` to inspect any operation and `abandon_move` to clear a stuck one.
184
+
185
+ ## Session Log
151
186
 
152
187
  The session log persists to `~/.icloud-mcp-session.json` on your Mac — outside Claude's context window — so progress is never lost during long operations. Claude can write its plan at the start, log each completed step, and read the log back at any point to reorient itself.
153
188
 
@@ -174,4 +209,4 @@ Once configured, you can ask Claude things like:
174
209
 
175
210
  ## License
176
211
 
177
- MIT
212
+ MIT
package/index.js CHANGED
@@ -15,8 +15,12 @@ const IMAP_USER = process.env.IMAP_USER;
15
15
  const IMAP_PASSWORD = process.env.IMAP_PASSWORD;
16
16
 
17
17
  if (!IMAP_USER || !IMAP_PASSWORD) {
18
- process.stderr.write('Error: IMAP_USER and IMAP_PASSWORD environment variables are required\n');
19
- process.exit(1);
18
+ if (process.argv.includes('--doctor')) {
19
+ // Doctor will handle missing credentials with friendly output
20
+ } else {
21
+ process.stderr.write('Error: IMAP_USER and IMAP_PASSWORD environment variables are required\n');
22
+ process.exit(1);
23
+ }
20
24
  }
21
25
 
22
26
  function createClient() {
@@ -1426,7 +1430,7 @@ async function main() {
1426
1430
  }
1427
1431
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1428
1432
  } catch (error) {
1429
- return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
1433
+ return { content: [{ type: 'text', text: `Error: ${friendlyError(error)}` }], isError: true };
1430
1434
  }
1431
1435
  });
1432
1436
 
@@ -1435,6 +1439,132 @@ async function main() {
1435
1439
  process.stderr.write('iCloud Mail MCP Server running\n');
1436
1440
  }
1437
1441
 
1442
+ // ─── Friendly error messages ──────────────────────────────────────────────────
1443
+
1444
+ function friendlyError(err) {
1445
+ const msg = err.message ?? '';
1446
+
1447
+ if (msg.includes('AUTHENTICATIONFAILED') || msg.includes('Invalid credentials') || msg.includes('Authentication failed')) {
1448
+ return [
1449
+ 'Authentication failed.',
1450
+ '→ Make sure IMAP_PASSWORD is an app-specific password, not your regular iCloud password.',
1451
+ '→ Generate one at: appleid.apple.com → Sign-In and Security → App-Specific Passwords',
1452
+ '→ Also check that IMAP_USER is your full iCloud email address (e.g. you@icloud.com)'
1453
+ ].join('\n');
1454
+ }
1455
+
1456
+ if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND') || msg.includes('ENETUNREACH')) {
1457
+ return [
1458
+ 'Could not reach imap.mail.me.com:993.',
1459
+ '→ Check your internet connection.',
1460
+ '→ If you are behind a firewall or VPN, port 993 may be blocked.'
1461
+ ].join('\n');
1462
+ }
1463
+
1464
+ if (msg.includes('ETIMEDOUT') || msg.includes('socket hang up')) {
1465
+ return [
1466
+ 'Connection to iCloud timed out.',
1467
+ '→ Check your internet connection and try again.',
1468
+ '→ iCloud IMAP can be slow under load — this is usually transient.'
1469
+ ].join('\n');
1470
+ }
1471
+
1472
+ if (msg.includes('ECONNRESET')) {
1473
+ return [
1474
+ 'iCloud closed the connection unexpectedly.',
1475
+ '→ This is usually transient. Try again in a few seconds.'
1476
+ ].join('\n');
1477
+ }
1478
+
1479
+ if (msg.includes('Mailbox does not exist') || msg.includes('does not exist') || msg.includes('NONEXISTENT')) {
1480
+ return [
1481
+ `Mailbox not found: ${msg}`,
1482
+ '→ Check the folder name is correct — iCloud folder names are case-sensitive.',
1483
+ '→ Use list_mailboxes to see all available folders.'
1484
+ ].join('\n');
1485
+ }
1486
+
1487
+ // Fall through — return original message
1488
+ return msg;
1489
+ }
1490
+
1491
+ // ─── Doctor command ───────────────────────────────────────────────────────────
1492
+
1493
+ async function runDoctor() {
1494
+ const divider = '─'.repeat(45);
1495
+ process.stdout.write(`\nicloud-mcp doctor\n${divider}\n`);
1496
+
1497
+ const checks = [
1498
+ {
1499
+ label: 'IMAP_USER is set',
1500
+ run: () => {
1501
+ if (!IMAP_USER) throw new Error('IMAP_USER environment variable is not set.\n→ Add it to your Claude Desktop config env block.');
1502
+ }
1503
+ },
1504
+ {
1505
+ label: 'IMAP_PASSWORD is set',
1506
+ run: () => {
1507
+ if (!IMAP_PASSWORD) throw new Error('IMAP_PASSWORD environment variable is not set.\n→ Add it to your Claude Desktop config env block.');
1508
+ }
1509
+ },
1510
+ {
1511
+ label: 'IMAP_USER looks like an email address',
1512
+ run: () => {
1513
+ if (!IMAP_USER?.includes('@')) throw new Error(`IMAP_USER "${IMAP_USER}" doesn't look like an email address.\n→ Use your full iCloud address, e.g. you@icloud.com`);
1514
+ }
1515
+ },
1516
+ {
1517
+ label: `Connected to imap.mail.me.com:993`,
1518
+ run: async () => {
1519
+ const client = createClient();
1520
+ await client.connect();
1521
+ await client.logout();
1522
+ }
1523
+ },
1524
+ {
1525
+ label: `Authenticated as ${IMAP_USER}`,
1526
+ run: async () => {
1527
+ // Auth is validated as part of connect — if we reach here it passed.
1528
+ // This check exists to give a clearer label in the output.
1529
+ }
1530
+ },
1531
+ {
1532
+ label: 'INBOX opened',
1533
+ run: async () => {
1534
+ const client = createClient();
1535
+ await client.connect();
1536
+ const mb = await client.mailboxOpen('INBOX');
1537
+ await client.logout();
1538
+ return `${mb.exists.toLocaleString()} messages`;
1539
+ }
1540
+ }
1541
+ ];
1542
+
1543
+ let allPassed = true;
1544
+
1545
+ for (const check of checks) {
1546
+ try {
1547
+ const detail = await check.run();
1548
+ const suffix = detail ? ` (${detail})` : '';
1549
+ process.stdout.write(`āœ… ${check.label}${suffix}\n`);
1550
+ } catch (err) {
1551
+ process.stdout.write(`āŒ ${check.label}\n ${friendlyError(err).replace(/\n/g, '\n ')}\n`);
1552
+ allPassed = false;
1553
+ break; // No point continuing after a failure
1554
+ }
1555
+ }
1556
+
1557
+ process.stdout.write(`${divider}\n`);
1558
+ if (allPassed) {
1559
+ process.stdout.write('All checks passed. Ready to use with Claude Desktop.\n\n');
1560
+ process.exit(0);
1561
+ } else {
1562
+ process.stdout.write('Setup is not complete. Fix the issue above and run --doctor again.\n\n');
1563
+ process.exit(1);
1564
+ }
1565
+ }
1566
+
1567
+
1438
1568
  process.on('uncaughtException', (err) => {
1439
1569
  process.stderr.write(`Uncaught exception: ${err.message}\n${err.stack}\n`);
1440
1570
  process.exit(1);
@@ -1445,7 +1575,14 @@ process.on('unhandledRejection', (reason) => {
1445
1575
  process.exit(1);
1446
1576
  });
1447
1577
 
1448
- main().catch((err) => {
1449
- process.stderr.write(`Fatal error: ${err.message}\n${err.stack}\n`);
1450
- process.exit(1);
1451
- });
1578
+ if (process.argv.includes('--doctor')) {
1579
+ runDoctor().catch((err) => {
1580
+ process.stderr.write(`Doctor failed unexpectedly: ${err.message}\n`);
1581
+ process.exit(1);
1582
+ });
1583
+ } else {
1584
+ main().catch((err) => {
1585
+ process.stderr.write(`Fatal error: ${err.message}\n${err.stack}\n`);
1586
+ process.exit(1);
1587
+ });
1588
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icloud-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "A Model Context Protocol (MCP) server for iCloud Mail",
5
5
  "main": "index.js",
6
6
  "bin": {