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.
- package/README.md +45 -10
- package/index.js +144 -7
- 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
|
-
|
|
46
|
+
The path varies by setup:
|
|
46
47
|
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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
|
+
}
|