apple-tools-mcp 2.0.4 → 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,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`).
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`).
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,118 @@ 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.
184
- *
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).
172
+ * AppleScript handlers: focus the compose body (not To) and paste.
173
+ * System Events needs Accessibility; Mail make/send still needs Automation.
191
174
  */
192
- export function buildMailtoComposeUrl({ subject = "", body = "" } = {}) {
193
- return `mailto:?subject=${encodeMailtoQueryValue(subject)}&body=${encodeMailtoQueryValue(body)}`;
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}`;
194
251
  }
195
252
 
196
253
  /**
197
254
  * Build the outgoing-message script shared by send and draft.
198
255
  *
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">`.
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).
207
265
  */
208
266
  export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
209
267
  const recipients = [
@@ -212,19 +270,32 @@ export function buildComposeScript({ to, cc, bcc, subject, body, send, html = fa
212
270
  recipientLines(bcc, "bcc recipient")
213
271
  ].filter((block) => block.length > 0).join("\n");
214
272
 
215
- const mailtoUrl = buildMailtoComposeUrl({
216
- subject,
217
- body: composeMailtoBody(body, { html })
218
- });
273
+ const plainBody = composeNativeBody(body, { html });
274
+ const pasteAndVerify = plainBody
275
+ ? ` atmPasteMailBody(${asString(plainBody)})
276
+ tell application "Mail"
277
+ ${recipientStillPresentChecks(to)}
278
+ end tell`
279
+ : "";
219
280
 
220
- return `tell application "Mail"
221
- set newMessage to mailto ${asString(mailtoUrl)}
222
- try
223
- set visible of newMessage to false
224
- end try
281
+ return `${buildMailBodyPasteHandler()}
282
+
283
+ tell application "Mail"
284
+ set newMessage to make new outgoing message with properties {subject:${asString(subject)}, visible:true}
225
285
  tell newMessage
226
286
  ${recipients}
227
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"
228
299
  ${send ? "send newMessage" : "save newMessage"}
229
300
  end tell
230
301
  return "OK"`;
@@ -437,7 +508,20 @@ export function recoverIfInSent({ inReplyTo = null, subject = null, forwardTo =
437
508
  return null;
438
509
  }
439
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
+
440
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
+ }
441
525
  if (result.kind === "tcc" || isHardTccDenial(result && result.error)) {
442
526
  return `${action} failed — attempted to ${summary}. ${MAIL_TCC_GUIDANCE}`;
443
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: "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.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",