apple-tools-mcp 2.0.4 → 2.0.6

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 CHANGED
@@ -461,11 +461,13 @@ Two arguments are available on **every** write tool:
461
461
  | `mail_archive` | `message_id` or `file_path` | none |
462
462
  | `mail_trash` | `message_id` or `file_path` | **`confirm` required** |
463
463
 
464
- `body_format: "html"` uses the same mailto compose path with a tag-stripped body. It does **not** set Mail's `html content` — that setter cite-wraps the whole message (`>` prefixes and `<blockquote type="cite">`, same iOS purple bar). Mail may still generate its own HTML alternative from the native compose. The default is plain text.
464
+ `body_format: "html"` uses the same compose path with a tag-stripped body pasted into Mail's native editor. It does **not** set Mail's `content` or `html content` — those setters cite-wrap the whole message (`>` prefixes and `<blockquote type="cite">`, same iOS purple bar). Mail may still generate its own HTML alternative from the native compose. The default is plain text.
465
465
 
466
- **Compose is not quoted.** `mail_send` / `mail_draft` (plain and html) create the outgoing message with Mail's `mailto` command (native compose) instead of setting AppleScript `content` or `html content` on `make new outgoing message`. On current Mail (Ventura+, FB11734014) those setters store the body as a citation: every plain-text line prefixed with `>`, plus a `multipart/alternative` HTML part wrapped in `<blockquote type="cite">`. Desktop Mail often hides the bar with inline styles; iOS Mail paints the whole body purple with a left quote bar — even when the subject is not `Re:`/`Fwd:` and there is no `In-Reply-To`. Reply and forward still quote the original, which is expected.
466
+ **Compose is not quoted.** `mail_send` / `mail_draft` (plain and html) call `make new outgoing message` **without** AppleScript `content:` so `newMessage` is a real Mail object, then paste the body into the compose window (System Events). Mail's `mailto` command was shipped in 2.0.4 and **fails on Mini/MacBook Mail**: it does not return an outgoing message (`newMessage` undefined, AppleScript **-2753**). On current Mail (Ventura+, FB11734014) `content` / `html content` store the body as a citation: every plain-text line prefixed with `>`, plus a `multipart/alternative` HTML part wrapped in `<blockquote type="cite">`. Desktop Mail often hides the bar with inline styles; iOS Mail paints the whole body purple with a left quote bar — even when the subject is not `Re:`/`Fwd:` and there is no `In-Reply-To`. Reply and forward still quote the original, which is expected.
467
467
 
468
- **Manual prove (after 2.0.4 is installed on the Mac host — post-publish, does not block this PR):** `mail_send` a short plain message and a short `body_format: "html"` message whose body looks like ordinary paragraphs (for example `<p>Quick note</p>`), neither with a `Re:`/`Fwd:` subject. Inspect each Sent `.emlx`: the text/plain part must not prefix every body line with `>`, and any HTML alternative must not wrap the whole body in `<blockquote type="cite">`.
468
+ Paste uses System Events, so **node needs Accessibility** (Privacy & Security → Accessibility) in addition to Automation → Mail. That Accessibility deny is not a Mail Automation deny (`-1743` / `-10004`). Body focus uses `text area` / `scroll area` and `UI element whose role is "AXWebArea"` — never the System Events class `web area`, which does not compile on macOS 26.x (**-2741**, “Expected class name but found identifier”).
469
+
470
+ **Manual prove (2.0.6 on the Mac host):** `mail_send` a short plain message and a short `body_format: "html"` message whose body looks like ordinary paragraphs (for example `<p>Quick note</p>`), neither with a `Re:`/`Fwd:` subject. Inspect each Sent `.emlx`: the text/plain part must not prefix every body line with `>`, and any HTML alternative must not wrap the whole body in `<blockquote type="cite">`. Compose must succeed (no `-2753` / undefined `newMessage`, no **-2741** on body focus).
469
471
 
470
472
  Emails are addressed by their RFC822 **Message-ID**. Pass `message_id`, or pass the `file_path` from `mail_search` / `mail_recent` and the server reads the Message-ID out of the `.emlx` headers for you. `mail_archive` moves the message to its account's Archive (or All Mail) mailbox; `mail_trash` moves it to that account's Trash.
471
473
 
@@ -179,6 +179,7 @@ export function needsHostTccAdvice(message) {
179
179
  text.includes(CALENDAR_TCC_GUIDANCE) ||
180
180
  text.includes(MESSAGES_TCC_GUIDANCE) ||
181
181
  text.includes(MAIL_TCC_GUIDANCE) ||
182
+ text.includes(MAIL_ACCESSIBILITY_GUIDANCE) ||
182
183
  text.includes(ATTRIBUTION_GUIDANCE) ||
183
184
  text.includes(TCC_GUIDANCE)
184
185
  ) {
@@ -332,6 +333,16 @@ export const MAIL_TCC_GUIDANCE =
332
333
  "Allow node in System Settings → Privacy & Security → Automation for Mail (same Allow-via-prompt as Contacts and Calendar — do not add node via + in a privacy list). " +
333
334
  "A Contacts or Calendar grant does not include Mail.";
334
335
 
336
+ /**
337
+ * Body paste for mail_send / mail_draft uses System Events. That is
338
+ * Accessibility, not Mail Automation (-1743).
339
+ */
340
+ export const MAIL_ACCESSIBILITY_GUIDANCE =
341
+ "macOS denied Accessibility (System Events could not drive Mail's compose window). " +
342
+ "mail_send / mail_draft paste the body so Mail does not cite-wrap it (FB11734014); Mail's mailto command does not return an outgoing message on current Mini/MacBook Mail (-2753). " +
343
+ "Allow node in System Settings → Privacy & Security → Accessibility, and keep Automation → Mail allowed. " +
344
+ "This is not a Mail Automation deny (-1743 / -10004).";
345
+
335
346
  /**
336
347
  * AppleScript hung (ETIMEDOUT / -1712) while finding, opening, replying,
337
348
  * or sending. That is not -1743 / -10004, including before send.
package/lib/mailWrite.js CHANGED
@@ -19,8 +19,10 @@ import { validateEmailPath, unfoldRfc822Headers, safeMatch, stripHtmlTags } from
19
19
  import {
20
20
  runAppleScript,
21
21
  asString,
22
+ asInteger,
22
23
  MAIL_TCC_GUIDANCE,
23
24
  MAIL_SEND_TIMEOUT_GUIDANCE,
25
+ MAIL_ACCESSIBILITY_GUIDANCE,
24
26
  MAIL_APP_NOT_RUNNING_GUIDANCE,
25
27
  ATTRIBUTION_GUIDANCE,
26
28
  isHardTccDenial
@@ -148,62 +150,159 @@ export function probeMailAutomation() {
148
150
  };
149
151
  }
150
152
 
151
- /**
152
- * Percent-encode a mailto query value (RFC 6068).
153
- *
154
- * Newlines become %0D%0A so Mail keeps paragraphs. Characters that
155
- * encodeURIComponent leaves unescaped (`!'()*`) are encoded too, because
156
- * they can break the URL or the AppleScript string around it.
157
- *
158
- * Does not prefix lines with `>` — Mail's native mailto compose path
159
- * already treats the decoded body as original text, not a citation.
160
- */
161
- export function encodeMailtoQueryValue(value) {
162
- const text = value === undefined || value === null ? "" : String(value);
163
- const normalized = text.replace(/\r\n|\r|\n/g, "\r\n");
164
- return encodeURIComponent(normalized).replace(/[!'()*]/g, (ch) => {
165
- return `%${ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`;
166
- });
167
- }
153
+ export const ACCESSIBILITY_DENIED_SENTINEL = "ACCESSIBILITY_DENIED";
154
+ export const BODY_PASTE_MISDIRECTED_SENTINEL = "BODY_PASTE_MISDIRECTED";
168
155
 
169
156
  /**
170
- * Body text for Mail's mailto compose.
157
+ * Body text for native Mail compose (clipboard paste, not AppleScript content).
171
158
  *
172
159
  * HTML is tag-stripped so a plain-looking HTML body (`<p>…</p>`) becomes
173
- * native compose text. Never prefixes lines with `>` and never emits
174
- * `<blockquote>` — AppleScript `html content` is unused because Mail
175
- * cite-wraps it (FB11734014).
160
+ * typed/pasted text. Never prefixes lines with `>` and never emits
161
+ * `<blockquote>` — AppleScript `content` / `html content` cite-wrap
162
+ * (FB11734014). Mail's `mailto` command is not used: on current Mini/MacBook
163
+ * Mail it does not return an outgoing message (`newMessage` stays undefined,
164
+ * AppleScript -2753).
176
165
  */
177
- export function composeMailtoBody(body, { html = false } = {}) {
166
+ export function composeNativeBody(body, { html = false } = {}) {
178
167
  if (html) return stripHtmlTags(body);
179
168
  return body === undefined || body === null ? "" : String(body);
180
169
  }
181
170
 
182
171
  /**
183
- * Build a mailto: URL for Mail's `mailto` command.
172
+ * AppleScript handlers: focus the compose body (not To) and paste.
173
+ * System Events needs Accessibility; Mail make/send still needs Automation.
184
174
  *
185
- * Recipients stay off this URL and are added with `make new … recipient`
186
- * so cc/bcc keep the proven AppleScript path and a To is not duplicated.
187
- * Subject and body go here so Mail uses its native compose pipeline
188
- * (FB11734014: setting AppleScript `content` wraps the whole body in a
189
- * cite-blockquote — `>` prefixes on text/plain, `<blockquote type="cite">`
190
- * on text/html — which iOS Mail paints as purple text with a left bar).
175
+ * macOS 26.x System Events has no `web area` class (compile -2741,
176
+ * "Expected class name but found identifier"). Hosts compile
177
+ * `text area` / `scroll area` and `UI element whose role is "AXWebArea"`.
191
178
  */
192
- export function buildMailtoComposeUrl({ subject = "", body = "" } = {}) {
193
- return `mailto:?subject=${encodeMailtoQueryValue(subject)}&body=${encodeMailtoQueryValue(body)}`;
179
+ export function buildMailBodyPasteHandler() {
180
+ return `on atmFocusMailBody()
181
+ tell application "System Events"
182
+ tell process "Mail"
183
+ set frontmost to true
184
+ if (count of windows) < 1 then error "MAIL_COMPOSE_WINDOW_MISSING"
185
+ set w to first window
186
+ try
187
+ set focused of text area 1 of scroll area 1 of w to true
188
+ return
189
+ end try
190
+ try
191
+ set focused of text area 1 of w to true
192
+ return
193
+ end try
194
+ try
195
+ set focused of (first UI element of w whose role is "AXTextArea") to true
196
+ return
197
+ end try
198
+ try
199
+ click (first UI element of w whose role is "AXWebArea")
200
+ return
201
+ end try
202
+ try
203
+ click (first UI element of scroll area 1 of w whose role is "AXWebArea")
204
+ return
205
+ end try
206
+ try
207
+ click (first UI element of scroll area 1 of splitter group 1 of w whose role is "AXWebArea")
208
+ return
209
+ end try
210
+ try
211
+ click (first UI element of scroll area 1 of group 1 of splitter group 1 of w whose role is "AXWebArea")
212
+ return
213
+ end try
214
+ set atmQueue to UI elements of w
215
+ set atmWalked to 0
216
+ repeat while (count of atmQueue) > 0 and atmWalked < 80
217
+ set atmWalked to atmWalked + 1
218
+ set atmElem to item 1 of atmQueue
219
+ if (count of atmQueue) is 1 then
220
+ set atmQueue to {}
221
+ else
222
+ set atmQueue to items 2 thru -1 of atmQueue
223
+ end if
224
+ try
225
+ if (role of atmElem as string) is "AXWebArea" then
226
+ click atmElem
227
+ return
228
+ end if
229
+ end try
230
+ try
231
+ if (role of atmElem as string) is "AXTextArea" then
232
+ set focused of atmElem to true
233
+ return
234
+ end if
235
+ end try
236
+ try
237
+ set atmKids to UI elements of atmElem
238
+ repeat with i from 1 to (count of atmKids)
239
+ set end of atmQueue to item i of atmKids
240
+ end repeat
241
+ end try
242
+ end repeat
243
+ set {wx, wy} to position of w
244
+ set {ww, wh} to size of w
245
+ click at {(wx + (ww div 2)) as integer, (wy + ((wh * 2) div 3)) as integer}
246
+ end tell
247
+ end tell
248
+ end atmFocusMailBody
249
+
250
+ on atmPasteMailBody(bodyText)
251
+ set savedClip to ""
252
+ set hadClip to false
253
+ try
254
+ set savedClip to the clipboard as text
255
+ set hadClip to true
256
+ end try
257
+ set the clipboard to bodyText
258
+ tell application "Mail" to activate
259
+ delay 0.25
260
+ try
261
+ atmFocusMailBody()
262
+ tell application "System Events"
263
+ tell process "Mail"
264
+ keystroke "v" using command down
265
+ end tell
266
+ end tell
267
+ delay 0.15
268
+ on error errMsg number errNum
269
+ if hadClip then set the clipboard to savedClip
270
+ if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
271
+ error errMsg number errNum
272
+ end try
273
+ if hadClip then set the clipboard to savedClip
274
+ end atmPasteMailBody`;
275
+ }
276
+
277
+ function recipientStillPresentChecks(to) {
278
+ const toCount = asInteger(to.length, { min: 1, max: 99, field: "toCount" });
279
+ const stillThere = to
280
+ .map((address, i) => {
281
+ const n = asInteger(i, { min: 0, max: 99, field: "recipientIndex" });
282
+ return ` set atmWanted${n} to ${asString(address)}
283
+ set atmFound${n} to false
284
+ repeat with r in to recipients of newMessage
285
+ if (address of r as string) is atmWanted${n} then set atmFound${n} to true
286
+ end repeat
287
+ if atmFound${n} is false then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"`;
288
+ })
289
+ .join("\n");
290
+ return ` if (count of to recipients of newMessage) is not ${toCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
291
+ ${stillThere}`;
194
292
  }
195
293
 
196
294
  /**
197
295
  * Build the outgoing-message script shared by send and draft.
198
296
  *
199
- * `mail_send` / `mail_draft` (plain and html) must not set AppleScript
200
- * `content` or `html content`. Those properties route through Mail's
201
- * share-insert path and serialize as a citation. Mail's `mailto` command
202
- * returns the outgoing message so we can still add recipients, hide the
203
- * window, and send or save — without Accessibility / System Events, and
204
- * without a quote-wrapped body. HTML format uses the same path with a
205
- * tag-stripped mailto body; Mail may generate its own HTML alternative
206
- * without wrapping the whole message in `<blockquote type="cite">`.
297
+ * Always `make new outgoing message` (no `content:`) so `newMessage` is a
298
+ * real Mail object — Mail's `mailto` command is a no-op / non-returning
299
+ * on Mini+MacBook and left `newMessage` undefined (-2753).
300
+ *
301
+ * Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
302
+ * cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
303
+ * Recipients are AppleScript `make new … recipient`. The body is pasted
304
+ * into the compose window (native editor) via System Events. If paste
305
+ * lands in To, recipient count changes and we abort + delete (nothing sent).
207
306
  */
208
307
  export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
209
308
  const recipients = [
@@ -212,19 +311,32 @@ export function buildComposeScript({ to, cc, bcc, subject, body, send, html = fa
212
311
  recipientLines(bcc, "bcc recipient")
213
312
  ].filter((block) => block.length > 0).join("\n");
214
313
 
215
- const mailtoUrl = buildMailtoComposeUrl({
216
- subject,
217
- body: composeMailtoBody(body, { html })
218
- });
314
+ const plainBody = composeNativeBody(body, { html });
315
+ const pasteAndVerify = plainBody
316
+ ? ` atmPasteMailBody(${asString(plainBody)})
317
+ tell application "Mail"
318
+ ${recipientStillPresentChecks(to)}
319
+ end tell`
320
+ : "";
219
321
 
220
- return `tell application "Mail"
221
- set newMessage to mailto ${asString(mailtoUrl)}
222
- try
223
- set visible of newMessage to false
224
- end try
322
+ return `${buildMailBodyPasteHandler()}
323
+
324
+ tell application "Mail"
325
+ set newMessage to make new outgoing message with properties {subject:${asString(subject)}, visible:true}
225
326
  tell newMessage
226
327
  ${recipients}
227
328
  end tell
329
+ activate
330
+ end tell
331
+ try
332
+ ${pasteAndVerify}
333
+ on error errMsg number errNum
334
+ try
335
+ tell application "Mail" to delete newMessage
336
+ end try
337
+ error errMsg number errNum
338
+ end try
339
+ tell application "Mail"
228
340
  ${send ? "send newMessage" : "save newMessage"}
229
341
  end tell
230
342
  return "OK"`;
@@ -437,7 +549,20 @@ export function recoverIfInSent({ inReplyTo = null, subject = null, forwardTo =
437
549
  return null;
438
550
  }
439
551
 
552
+ export function isMailAccessibilityDenial(result) {
553
+ if (!result) return false;
554
+ const text = String(result.error || "").toLowerCase();
555
+ return text.includes("accessibility_denied") || text.includes("assistive access");
556
+ }
557
+
440
558
  function failure(action, summary, result, secrets) {
559
+ const err = String((result && result.error) || "").toLowerCase();
560
+ if (err.includes("body_paste_misdirected")) {
561
+ return `${action} failed — attempted to ${summary}. The body was pasted into a header field instead of the message body; nothing was sent.`;
562
+ }
563
+ if (isMailAccessibilityDenial(result)) {
564
+ return `${action} failed — attempted to ${summary}. ${MAIL_ACCESSIBILITY_GUIDANCE}`;
565
+ }
441
566
  if (result.kind === "tcc" || isHardTccDenial(result && result.error)) {
442
567
  return `${action} failed — attempted to ${summary}. ${MAIL_TCC_GUIDANCE}`;
443
568
  }
package/lib/writeTools.js CHANGED
@@ -57,7 +57,7 @@ export const WRITE_TOOL_DEFINITIONS = [
57
57
  bcc: { type: "array", items: { type: "string" }, description: "BCC email addresses" },
58
58
  subject: { type: "string", description: "Subject line (required)" },
59
59
  body: { type: "string", description: "Message body (required)" },
60
- body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped onto Mail's mailto compose; AppleScript html content is not set because it quote-wraps the Sent body" },
60
+ body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped and pasted into Mail's compose window; AppleScript content/html content is not set because it quote-wraps the Sent body" },
61
61
  ...CONFIRM_PROPS
62
62
  },
63
63
  required: ["to", "subject", "body"]
@@ -74,7 +74,7 @@ export const WRITE_TOOL_DEFINITIONS = [
74
74
  bcc: { type: "array", items: { type: "string" }, description: "BCC email addresses" },
75
75
  subject: { type: "string", description: "Subject line" },
76
76
  body: { type: "string", description: "Message body" },
77
- body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped onto Mail's mailto compose; AppleScript html content is not set because it quote-wraps the Sent body" },
77
+ body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped and pasted into Mail's compose window; AppleScript content/html content is not set because it quote-wraps the Sent body" },
78
78
  ...CONFIRM_PROPS
79
79
  },
80
80
  required: ["to"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-tools-mcp",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "MCP server for semantic search and write actions across Apple Mail, Messages, Calendar, and Contacts",
5
5
  "type": "module",
6
6
  "main": "index.js",