apple-tools-mcp 2.0.3 → 2.0.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 CHANGED
@@ -461,7 +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"` sets Mail's `html content` and keeps a tag-stripped plain-text alternative in `content`; the default is plain text. HTML support is whatever Mail.app offers — if a Mail version rejects the property, the message still goes out as 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
+
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
+
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`).
469
+
470
+ **Manual prove (2.0.5 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`).
465
471
 
466
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.
467
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,11 +150,118 @@ export function probeMailAutomation() {
148
150
  };
149
151
  }
150
152
 
153
+ export const ACCESSIBILITY_DENIED_SENTINEL = "ACCESSIBILITY_DENIED";
154
+ export const BODY_PASTE_MISDIRECTED_SENTINEL = "BODY_PASTE_MISDIRECTED";
155
+
156
+ /**
157
+ * Body text for native Mail compose (clipboard paste, not AppleScript content).
158
+ *
159
+ * HTML is tag-stripped so a plain-looking HTML body (`<p>…</p>`) becomes
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).
165
+ */
166
+ export function composeNativeBody(body, { html = false } = {}) {
167
+ if (html) return stripHtmlTags(body);
168
+ return body === undefined || body === null ? "" : String(body);
169
+ }
170
+
171
+ /**
172
+ * AppleScript handlers: focus the compose body (not To) and paste.
173
+ * System Events needs Accessibility; Mail make/send still needs Automation.
174
+ */
175
+ export function buildMailBodyPasteHandler() {
176
+ return `on atmFocusMailBody()
177
+ tell application "System Events"
178
+ tell process "Mail"
179
+ set frontmost to true
180
+ if (count of windows) < 1 then error "MAIL_COMPOSE_WINDOW_MISSING"
181
+ set w to first window
182
+ try
183
+ set focused of text area 1 of scroll area 1 of w to true
184
+ return
185
+ end try
186
+ try
187
+ click (first web area of w)
188
+ return
189
+ end try
190
+ try
191
+ click (first web area of scroll area 1 of w)
192
+ return
193
+ end try
194
+ try
195
+ click (first web area of scroll area 1 of splitter group 1 of w)
196
+ return
197
+ end try
198
+ try
199
+ click (first web area of scroll area 1 of group 1 of splitter group 1 of w)
200
+ return
201
+ end try
202
+ set {wx, wy} to position of w
203
+ set {ww, wh} to size of w
204
+ click at {(wx + (ww div 2)) as integer, (wy + ((wh * 2) div 3)) as integer}
205
+ end tell
206
+ end tell
207
+ end atmFocusMailBody
208
+
209
+ on atmPasteMailBody(bodyText)
210
+ set savedClip to ""
211
+ set hadClip to false
212
+ try
213
+ set savedClip to the clipboard as text
214
+ set hadClip to true
215
+ end try
216
+ set the clipboard to bodyText
217
+ tell application "Mail" to activate
218
+ delay 0.25
219
+ try
220
+ atmFocusMailBody()
221
+ tell application "System Events"
222
+ tell process "Mail"
223
+ keystroke "v" using command down
224
+ end tell
225
+ end tell
226
+ delay 0.15
227
+ on error errMsg number errNum
228
+ if hadClip then set the clipboard to savedClip
229
+ if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
230
+ error errMsg number errNum
231
+ end try
232
+ if hadClip then set the clipboard to savedClip
233
+ end atmPasteMailBody`;
234
+ }
235
+
236
+ function recipientStillPresentChecks(to) {
237
+ const toCount = asInteger(to.length, { min: 1, max: 99, field: "toCount" });
238
+ const stillThere = to
239
+ .map((address, i) => {
240
+ const n = asInteger(i, { min: 0, max: 99, field: "recipientIndex" });
241
+ return ` set atmWanted${n} to ${asString(address)}
242
+ set atmFound${n} to false
243
+ repeat with r in to recipients of newMessage
244
+ if (address of r as string) is atmWanted${n} then set atmFound${n} to true
245
+ end repeat
246
+ if atmFound${n} is false then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"`;
247
+ })
248
+ .join("\n");
249
+ return ` if (count of to recipients of newMessage) is not ${toCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
250
+ ${stillThere}`;
251
+ }
252
+
151
253
  /**
152
254
  * Build the outgoing-message script shared by send and draft.
153
255
  *
154
- * Mail renders `html content` when it is set; `content` stays populated as
155
- * the plain-text alternative for clients that do not display HTML.
256
+ * Always `make new outgoing message` (no `content:`) so `newMessage` is a
257
+ * real Mail object — Mail's `mailto` command is a no-op / non-returning
258
+ * on Mini+MacBook and left `newMessage` undefined (-2753).
259
+ *
260
+ * Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
261
+ * cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
262
+ * Recipients are AppleScript `make new … recipient`. The body is pasted
263
+ * into the compose window (native editor) via System Events. If paste
264
+ * lands in To, recipient count changes and we abort + delete (nothing sent).
156
265
  */
157
266
  export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
158
267
  const recipients = [
@@ -161,17 +270,32 @@ export function buildComposeScript({ to, cc, bcc, subject, body, send, html = fa
161
270
  recipientLines(bcc, "bcc recipient")
162
271
  ].filter((block) => block.length > 0).join("\n");
163
272
 
164
- const htmlLine = html
165
- ? ` try
166
- set html content of newMessage to ${asString(body)}
167
- end try\n`
273
+ const plainBody = composeNativeBody(body, { html });
274
+ const pasteAndVerify = plainBody
275
+ ? ` atmPasteMailBody(${asString(plainBody)})
276
+ tell application "Mail"
277
+ ${recipientStillPresentChecks(to)}
278
+ end tell`
168
279
  : "";
169
280
 
170
- return `tell application "Mail"
171
- set newMessage to make new outgoing message with properties {subject:${asString(subject)}, content:${asString(html ? stripHtmlTags(body) : body)}, visible:false}
172
- ${htmlLine} tell newMessage
281
+ return `${buildMailBodyPasteHandler()}
282
+
283
+ tell application "Mail"
284
+ set newMessage to make new outgoing message with properties {subject:${asString(subject)}, visible:true}
285
+ tell newMessage
173
286
  ${recipients}
174
287
  end tell
288
+ activate
289
+ end tell
290
+ try
291
+ ${pasteAndVerify}
292
+ on error errMsg number errNum
293
+ try
294
+ tell application "Mail" to delete newMessage
295
+ end try
296
+ error errMsg number errNum
297
+ end try
298
+ tell application "Mail"
175
299
  ${send ? "send newMessage" : "save newMessage"}
176
300
  end tell
177
301
  return "OK"`;
@@ -384,7 +508,20 @@ export function recoverIfInSent({ inReplyTo = null, subject = null, forwardTo =
384
508
  return null;
385
509
  }
386
510
 
511
+ export function isMailAccessibilityDenial(result) {
512
+ if (!result) return false;
513
+ const text = String(result.error || "").toLowerCase();
514
+ return text.includes("accessibility_denied") || text.includes("assistive access");
515
+ }
516
+
387
517
  function failure(action, summary, result, secrets) {
518
+ const err = String((result && result.error) || "").toLowerCase();
519
+ if (err.includes("body_paste_misdirected")) {
520
+ return `${action} failed — attempted to ${summary}. The body was pasted into a header field instead of the message body; nothing was sent.`;
521
+ }
522
+ if (isMailAccessibilityDenial(result)) {
523
+ return `${action} failed — attempted to ${summary}. ${MAIL_ACCESSIBILITY_GUIDANCE}`;
524
+ }
388
525
  if (result.kind === "tcc" || isHardTccDenial(result && result.error)) {
389
526
  return `${action} failed — attempted to ${summary}. ${MAIL_TCC_GUIDANCE}`;
390
527
  }
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: "Send body as plain text (default) or HTML, where Mail supports it" },
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: "Save body as plain text (default) or HTML, where Mail supports it" },
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.3",
3
+ "version": "2.0.5",
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",