apple-icloud-mcp 0.2.1 → 0.3.0

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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Unofficial Apple Music, iCloud Calendar/Contacts/Mail, Apple Maps and WeatherKit tools for Claude Code",
9
- "version": "0.2.1"
9
+ "version": "0.3.0"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -14,7 +14,7 @@
14
14
  "displayName": "Apple iCloud",
15
15
  "source": "./",
16
16
  "description": "Apple Music playlists and library, iCloud Calendar, Contacts and Mail, Apple Maps, WeatherKit and iTunes search for Claude via MCP — runs anywhere, no Mac required. Unofficial; not affiliated with Apple.",
17
- "version": "0.2.1",
17
+ "version": "0.3.0",
18
18
  "author": {
19
19
  "name": "Chris Chall"
20
20
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "apple-icloud",
3
3
  "displayName": "Apple iCloud",
4
- "version": "0.2.1",
4
+ "version": "0.3.0",
5
5
  "description": "Apple Music playlists and library, iCloud Calendar, Contacts and Mail, Apple Maps, WeatherKit and iTunes search for Claude via MCP — runs anywhere, no Mac required. Unofficial; not affiliated with Apple.",
6
6
  "author": {
7
7
  "name": "Chris Chall"
package/README.md CHANGED
@@ -312,6 +312,7 @@ recorded on disk, so a shared secret does not let one be replayed.
312
312
  | `apple_mail_list_mailboxes` | List the iCloud Mail mailboxes (folders): path, name, special use (inbox, sent, drafts, trash, junk, archive) and, by default, message and unread counts. | read | |
313
313
  | `apple_mail_search` | Search emails in one iCloud Mail mailbox (default INBOX) by sender, recipient, subject, full text, received date range, unread and flagged state. | read | |
314
314
  | `apple_mail_get_message` | Read one iCloud Mail message by uid (from apple_mail_search): headers (from, to, cc, reply-to, date, subject, message-id), the body as plain text (HTML converted to readable text when there is no text part), a truncated flag, and attachm… | read | |
315
+ | `apple_mail_download_attachment` | Download one attachment as an embedded binary resource using its 1-based index from `apple_mail_get_message`; defaults to 10 MiB, max 20 MiB, and never marks the message read. | read | |
315
316
  | `apple_mail_send` | Send a plain-text email from your iCloud Mail address (to/cc/bcc, subject, body; no attachments). | all | yes |
316
317
  | `apple_mail_update_flags` | Mark iCloud Mail messages read or unread, and flag or unflag them, by uid (1–100 uids from apple_mail_search, one mailbox). | all | |
317
318
  | `apple_mail_move` | Move iCloud Mail messages (1–100 uids from apple_mail_search, one mailbox) to another mailbox: a path from apple_mail_list_mailboxes or an alias (inbox, archive, trash, junk, sent, drafts). | all | |
@@ -444,7 +445,7 @@ All optional; each service activates when its credentials are present. Values th
444
445
  Notes and Photos natively.
445
446
  - **Playing music.** Apple's web APIs manage your library; playback happens in an Apple Music app.
446
447
  - **Creating or deleting calendars** — iCloud's CalDAV server does not reliably support it.
447
- - **Mail attachments** — sending is plain text; reading lists attachments but does not download them.
448
+ - **Mail attachment sending** — outgoing mail is plain text. Incoming attachments can be downloaded with `apple_mail_download_attachment`.
448
449
 
449
450
  ## Troubleshooting
450
451
 
package/dist/bundle.js CHANGED
@@ -36450,7 +36450,7 @@ var VERSION;
36450
36450
  var init_version = __esm({
36451
36451
  "src/version.ts"() {
36452
36452
  "use strict";
36453
- VERSION = "0.2.1";
36453
+ VERSION = "0.3.0";
36454
36454
  }
36455
36455
  });
36456
36456
 
@@ -115391,11 +115391,17 @@ function extractBody(email3, maxChars) {
115391
115391
  const truncated = full.length > maxChars;
115392
115392
  return { format, text: truncated ? full.slice(0, maxChars) : full, totalChars: full.length, truncated };
115393
115393
  }
115394
+ function attachmentBytes(content) {
115395
+ if (typeof content === "string") return Buffer.from(content);
115396
+ if (content instanceof ArrayBuffer) return Buffer.from(new Uint8Array(content));
115397
+ return Buffer.from(content);
115398
+ }
115394
115399
  function attachmentInfo(email3) {
115395
- return email3.attachments.map((a) => {
115400
+ return email3.attachments.map((a, i) => {
115396
115401
  const content = a.content;
115397
115402
  const size = typeof content === "string" ? Buffer.byteLength(content) : content.byteLength;
115398
115403
  return {
115404
+ index: i + 1,
115399
115405
  ...a.filename ? { filename: a.filename } : {},
115400
115406
  mimeType: a.mimeType,
115401
115407
  size,
@@ -117686,6 +117692,88 @@ function registerMailTools(server, deps = {}) {
117686
117692
  });
117687
117693
  }
117688
117694
  });
117695
+ defineTool(server, {
117696
+ name: "apple_mail_download_attachment",
117697
+ service: "mail",
117698
+ access: "read",
117699
+ title: "Download an iCloud Mail attachment",
117700
+ description: `Download one attachment from an iCloud Mail message as an embedded binary resource. Use the 1-based index shown by apple_mail_get_message. Reads the message without marking it read. Defaults to a 10 MiB attachment limit (maximum ${MAX_ATTACHMENT_BYTES / (1024 * 1024)} MiB). ${PREREQ}`,
117701
+ inputSchema: external_exports.strictObject({
117702
+ mailbox: mailboxParam.optional().describe("Mailbox holding the message (default inbox); path or alias."),
117703
+ uid: uidParam.describe("The message UID from apple_mail_search."),
117704
+ uidValidity: uidValidityParam,
117705
+ attachmentIndex: external_exports.number().int().min(1).max(100).describe("1-based attachment index from apple_mail_get_message."),
117706
+ maxBytes: external_exports.number().int().min(1).max(MAX_ATTACHMENT_BYTES).optional().describe(`Maximum decoded attachment bytes to return (default ${DEFAULT_ATTACHMENT_BYTES}; maximum ${MAX_ATTACHMENT_BYTES}).`)
117707
+ }),
117708
+ annotations: ANNOTATIONS.read,
117709
+ handler: async (args) => {
117710
+ const account = resolveMailAccount();
117711
+ const maxBytes = args.maxBytes ?? DEFAULT_ATTACHMENT_BYTES;
117712
+ return withImap(createImap(), account, async ({ client }) => {
117713
+ const path2 = await step("finding the mailbox", () => resolveMailbox(client, args.mailbox ?? "INBOX"));
117714
+ const { lock, uidValidity } = await step(
117715
+ `opening "${path2}"`,
117716
+ () => openMailbox(client, path2, { write: false, uidValidity: args.uidValidity })
117717
+ );
117718
+ try {
117719
+ const msg = await step(
117720
+ "reading the message attachment",
117721
+ () => client.fetchOne(String(args.uid), { uid: true, size: true, source: { maxLength: MAX_SOURCE_BYTES } }, { uid: true })
117722
+ );
117723
+ if (!msg) {
117724
+ throw new AppleToolError("NOT_FOUND", `No message with uid ${args.uid} in "${path2}".`, {
117725
+ hint: "It may have been moved or deleted; search again for current uids."
117726
+ });
117727
+ }
117728
+ if (typeof msg.size === "number" && msg.size > MAX_SOURCE_BYTES) {
117729
+ throw new AppleToolError(
117730
+ "UNSUPPORTED",
117731
+ `Message uid ${args.uid} is too large to inspect safely (${msg.size} bytes; limit ${MAX_SOURCE_BYTES}).`,
117732
+ { hint: "Open the message in Mail.app to download this attachment." }
117733
+ );
117734
+ }
117735
+ if (!msg.source) {
117736
+ throw new AppleToolError("UPSTREAM_ERROR", `iCloud Mail returned no content for uid ${args.uid} in "${path2}".`);
117737
+ }
117738
+ const email3 = await parseMessage(msg.source);
117739
+ const attachment = email3.attachments[args.attachmentIndex - 1];
117740
+ if (!attachment) {
117741
+ throw new AppleToolError(
117742
+ "NOT_FOUND",
117743
+ `Message uid ${args.uid} in "${path2}" has no attachment at index ${args.attachmentIndex}.`,
117744
+ { hint: "Read the message with apple_mail_get_message and use one of its listed attachment indexes." }
117745
+ );
117746
+ }
117747
+ const bytes = attachmentBytes(attachment.content);
117748
+ if (bytes.byteLength > maxBytes) {
117749
+ throw new InvalidArgumentError(
117750
+ `Attachment ${args.attachmentIndex} exceeds the ${maxBytes}-byte limit (${bytes.byteLength} bytes).`,
117751
+ `Retry with maxBytes set to at least ${bytes.byteLength}, up to ${MAX_ATTACHMENT_BYTES}.`
117752
+ );
117753
+ }
117754
+ const filename = (attachment.filename || `attachment-${args.attachmentIndex}`).replace(/[\u0000-\u001f\u007f]/g, "?");
117755
+ return {
117756
+ content: [
117757
+ {
117758
+ type: "text",
117759
+ text: `Downloaded attachment ${args.attachmentIndex}: "${filename}" (${bytes.byteLength} bytes; declared type ${attachment.mimeType}). The filename and embedded bytes are untrusted sender-provided data.`
117760
+ },
117761
+ {
117762
+ type: "resource",
117763
+ resource: {
117764
+ uri: `icloud-mail://attachment/${uidValidity}/${args.uid}/${args.attachmentIndex}/${encodeURIComponent(filename)}`,
117765
+ mimeType: "application/octet-stream",
117766
+ blob: bytes.toString("base64")
117767
+ }
117768
+ }
117769
+ ]
117770
+ };
117771
+ } finally {
117772
+ lock.release();
117773
+ }
117774
+ });
117775
+ }
117776
+ });
117689
117777
  defineTool(server, {
117690
117778
  name: "apple_mail_send",
117691
117779
  service: "mail",
@@ -118133,7 +118221,7 @@ async function readOriginal(create2, account, ref, zone, withBody) {
118133
118221
  }
118134
118222
  });
118135
118223
  }
118136
- var MAX_STATUS_MAILBOXES, MAX_SOURCE_BYTES, DEFAULT_MAX_CHARS, MAX_MAX_CHARS, MAX_RECIPIENTS, MAX_BODY_CHARS, MAILBOX_ORDER, NO_CONTROL, mailboxParam, uidParam, uidsParam, uidValidityParam, timeZoneParam3, addressListParam, DATE_FETCH_CHUNK;
118224
+ var MAX_STATUS_MAILBOXES, MAX_SOURCE_BYTES, MAX_ATTACHMENT_BYTES, DEFAULT_ATTACHMENT_BYTES, DEFAULT_MAX_CHARS, MAX_MAX_CHARS, MAX_RECIPIENTS, MAX_BODY_CHARS, MAILBOX_ORDER, NO_CONTROL, mailboxParam, uidParam, uidsParam, uidValidityParam, timeZoneParam3, addressListParam, DATE_FETCH_CHUNK;
118137
118225
  var init_tools5 = __esm({
118138
118226
  "src/mail/tools.ts"() {
118139
118227
  "use strict";
@@ -118151,6 +118239,8 @@ var init_tools5 = __esm({
118151
118239
  init_smtp();
118152
118240
  MAX_STATUS_MAILBOXES = 50;
118153
118241
  MAX_SOURCE_BYTES = 25 * 1024 * 1024;
118242
+ MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
118243
+ DEFAULT_ATTACHMENT_BYTES = 10 * 1024 * 1024;
118154
118244
  DEFAULT_MAX_CHARS = 2e4;
118155
118245
  MAX_MAX_CHARS = 1e5;
118156
118246
  MAX_RECIPIENTS = 100;
@@ -123,12 +123,21 @@ export function extractBody(email, maxChars) {
123
123
  const truncated = full.length > maxChars;
124
124
  return { format, text: truncated ? full.slice(0, maxChars) : full, totalChars: full.length, truncated };
125
125
  }
126
+ /** Convert PostalMime's attachment payload variants to exact bytes. */
127
+ export function attachmentBytes(content) {
128
+ if (typeof content === 'string')
129
+ return Buffer.from(content);
130
+ if (content instanceof ArrayBuffer)
131
+ return Buffer.from(new Uint8Array(content));
132
+ return Buffer.from(content);
133
+ }
126
134
  /** Attachment metadata only — never the bytes. */
127
135
  export function attachmentInfo(email) {
128
- return email.attachments.map((a) => {
136
+ return email.attachments.map((a, i) => {
129
137
  const content = a.content;
130
138
  const size = typeof content === 'string' ? Buffer.byteLength(content) : content.byteLength;
131
139
  return {
140
+ index: i + 1,
132
141
  ...(a.filename ? { filename: a.filename } : {}),
133
142
  mimeType: a.mimeType,
134
143
  size,
@@ -8,7 +8,7 @@ import { ANNOTATIONS, compactObject, defineTool, jsonResponse, limitParam, offse
8
8
  import { SMTP_HOST, SMTP_PORT, assertHttpProxy, mailProxyFor, mailTimeoutMs, resolveMailAccount, } from './config.js';
9
9
  import { distinctReplyTo, formatAddressList, specialUseWord, toDate, toRow } from './format.js';
10
10
  import { defaultCreateImapClient, mapImapError, mapImapWriteError, openMailbox, resolveMailbox, swallowedWriteFailure, uidSet, withImap, } from './imap.js';
11
- import { attachmentInfo, buildMessage, extractBody, parseMessage, parseRecipients, quoteOriginal, recipientLabel, replyReferences, replySubject, } from './message.js';
11
+ import { attachmentBytes, attachmentInfo, buildMessage, extractBody, parseMessage, parseRecipients, quoteOriginal, recipientLabel, replyReferences, replySubject, } from './message.js';
12
12
  import { classifySmtpError, createSmtpTransport } from './smtp.js';
13
13
  // ---------------------------------------------------------------------------
14
14
  // Limits
@@ -17,6 +17,9 @@ import { classifySmtpError, createSmtpTransport } from './smtp.js';
17
17
  export const MAX_STATUS_MAILBOXES = 50;
18
18
  /** Most of a message read into memory (iCloud's own cap is 20 MB per message). */
19
19
  export const MAX_SOURCE_BYTES = 25 * 1024 * 1024;
20
+ /** One attachment returned to the MCP client; larger payloads need a future streaming transfer path. */
21
+ export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
22
+ export const DEFAULT_ATTACHMENT_BYTES = 10 * 1024 * 1024;
20
23
  export const DEFAULT_MAX_CHARS = 20_000;
21
24
  export const MAX_MAX_CHARS = 100_000;
22
25
  export const MAX_RECIPIENTS = 100;
@@ -509,7 +512,83 @@ export function registerMailTools(server, deps = {}) {
509
512
  });
510
513
  },
511
514
  });
512
- // 4. send ------------------------------------------------------------------------
515
+ // 4. download attachment ---------------------------------------------------------
516
+ defineTool(server, {
517
+ name: 'apple_mail_download_attachment',
518
+ service: 'mail',
519
+ access: 'read',
520
+ title: 'Download an iCloud Mail attachment',
521
+ description: 'Download one attachment from an iCloud Mail message as an embedded binary resource. Use the 1-based index ' +
522
+ 'shown by apple_mail_get_message. Reads the message without marking it read. Defaults to a 10 MiB attachment ' +
523
+ `limit (maximum ${MAX_ATTACHMENT_BYTES / (1024 * 1024)} MiB). ${PREREQ}`,
524
+ inputSchema: z.strictObject({
525
+ mailbox: mailboxParam.optional().describe('Mailbox holding the message (default inbox); path or alias.'),
526
+ uid: uidParam.describe('The message UID from apple_mail_search.'),
527
+ uidValidity: uidValidityParam,
528
+ attachmentIndex: z.number().int().min(1).max(100).describe('1-based attachment index from apple_mail_get_message.'),
529
+ maxBytes: z
530
+ .number()
531
+ .int()
532
+ .min(1)
533
+ .max(MAX_ATTACHMENT_BYTES)
534
+ .optional()
535
+ .describe(`Maximum decoded attachment bytes to return (default ${DEFAULT_ATTACHMENT_BYTES}; maximum ${MAX_ATTACHMENT_BYTES}).`),
536
+ }),
537
+ annotations: ANNOTATIONS.read,
538
+ handler: async (args) => {
539
+ const account = resolveMailAccount();
540
+ const maxBytes = args.maxBytes ?? DEFAULT_ATTACHMENT_BYTES;
541
+ return withImap(createImap(), account, async ({ client }) => {
542
+ const path = await step('finding the mailbox', () => resolveMailbox(client, args.mailbox ?? 'INBOX'));
543
+ const { lock, uidValidity } = await step(`opening "${path}"`, () => openMailbox(client, path, { write: false, uidValidity: args.uidValidity }));
544
+ try {
545
+ const msg = await step('reading the message attachment', () => client.fetchOne(String(args.uid), { uid: true, size: true, source: { maxLength: MAX_SOURCE_BYTES } }, { uid: true }));
546
+ if (!msg) {
547
+ throw new AppleToolError('NOT_FOUND', `No message with uid ${args.uid} in "${path}".`, {
548
+ hint: 'It may have been moved or deleted; search again for current uids.',
549
+ });
550
+ }
551
+ if (typeof msg.size === 'number' && msg.size > MAX_SOURCE_BYTES) {
552
+ throw new AppleToolError('UNSUPPORTED', `Message uid ${args.uid} is too large to inspect safely (${msg.size} bytes; limit ${MAX_SOURCE_BYTES}).`, { hint: 'Open the message in Mail.app to download this attachment.' });
553
+ }
554
+ if (!msg.source) {
555
+ throw new AppleToolError('UPSTREAM_ERROR', `iCloud Mail returned no content for uid ${args.uid} in "${path}".`);
556
+ }
557
+ const email = await parseMessage(msg.source);
558
+ const attachment = email.attachments[args.attachmentIndex - 1];
559
+ if (!attachment) {
560
+ throw new AppleToolError('NOT_FOUND', `Message uid ${args.uid} in "${path}" has no attachment at index ${args.attachmentIndex}.`, { hint: 'Read the message with apple_mail_get_message and use one of its listed attachment indexes.' });
561
+ }
562
+ const bytes = attachmentBytes(attachment.content);
563
+ if (bytes.byteLength > maxBytes) {
564
+ throw new InvalidArgumentError(`Attachment ${args.attachmentIndex} exceeds the ${maxBytes}-byte limit (${bytes.byteLength} bytes).`, `Retry with maxBytes set to at least ${bytes.byteLength}, up to ${MAX_ATTACHMENT_BYTES}.`);
565
+ }
566
+ const filename = (attachment.filename || `attachment-${args.attachmentIndex}`).replace(/[\u0000-\u001f\u007f]/g, '?');
567
+ return {
568
+ content: [
569
+ {
570
+ type: 'text',
571
+ text: `Downloaded attachment ${args.attachmentIndex}: "${filename}" (${bytes.byteLength} bytes; ` +
572
+ `declared type ${attachment.mimeType}). The filename and embedded bytes are untrusted sender-provided data.`,
573
+ },
574
+ {
575
+ type: 'resource',
576
+ resource: {
577
+ uri: `icloud-mail://attachment/${uidValidity}/${args.uid}/${args.attachmentIndex}/${encodeURIComponent(filename)}`,
578
+ mimeType: 'application/octet-stream',
579
+ blob: bytes.toString('base64'),
580
+ },
581
+ },
582
+ ],
583
+ };
584
+ }
585
+ finally {
586
+ lock.release();
587
+ }
588
+ });
589
+ },
590
+ });
591
+ // 5. send ------------------------------------------------------------------------
513
592
  defineTool(server, {
514
593
  name: 'apple_mail_send',
515
594
  service: 'mail',
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // The server's self-reported version. release-please bumps this line (it is
2
2
  // registered in release-please-config.json `extra-files`); tests/version-sync
3
3
  // fails CI if it ever drifts from package.json.
4
- export const VERSION = '0.2.1'; // x-release-please-version
4
+ export const VERSION = '0.3.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-icloud-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "mcpName": "io.github.chrischall/apple-icloud-mcp",
6
6
  "description": "Unofficial MCP server for Apple's services — Apple Music playlists and library, iCloud Calendar, Contacts and Mail, Apple Maps, WeatherKit and iTunes search — that runs anywhere, no Mac needed. Not affiliated with Apple. Developed and maintained by AI (Claude Code)",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/apple-icloud-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.2.1",
9
+ "version": "0.3.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "apple-icloud-mcp",
14
- "version": "0.2.1",
14
+ "version": "0.3.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },