apple-tools-mcp 3.0.4 → 3.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/index.js CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  isWriteTool,
33
33
  dispatchWriteTool,
34
34
  executeWriteToolLocally,
35
+ loggedBridgeHandler,
35
36
  mcpWriteResult
36
37
  } from "./lib/writeTools.js";
37
38
  import { startWriteBridgeServer, defaultSocketPath } from "./lib/writeBridge.js";
@@ -112,7 +113,7 @@ async function startWriteBridge() {
112
113
  try {
113
114
  writeBridge = await startWriteBridgeServer({
114
115
  socketPath: WRITE_SOCKET_PATH,
115
- handler: (tool, args) => executeWriteToolLocally(tool, args),
116
+ handler: loggedBridgeHandler((tool, args) => executeWriteToolLocally(tool, args), (msg) => console.error(msg)),
116
117
  log: (msg) => console.error(msg)
117
118
  });
118
119
  ensureEventKitSession({ helpers: EVENTKIT_JXA_HELPERS });
package/lib/writeTools.js CHANGED
@@ -385,6 +385,46 @@ export function executeWriteToolLocally(name, args = {}) {
385
385
  * @param {object} [deps] - injected for tests
386
386
  * @returns {Promise<{ ok: boolean, message: string }>}
387
387
  */
388
+ /**
389
+ * One-word reason for a failed write, for logs. Never includes addresses,
390
+ * subjects, or bodies (write results echo the recipient and subject).
391
+ */
392
+ export function classifyWriteFailure(message) {
393
+ const t = String(message || "");
394
+ if (/GUI scripting is unavailable/i.test(t)) return "gui_scripting_unavailable";
395
+ if (/denied Accessibility|assistive access|-25211/i.test(t)) return "accessibility_denied";
396
+ if (/display is asleep|screen saver/i.test(t)) return "display_asleep_or_screen_saver";
397
+ if (/focus proof failed|caret|Keyboard focus/i.test(t)) return "compose_focus";
398
+ if (/timed out|ETIMEDOUT|-1712/i.test(t)) return "timeout";
399
+ if (/-1743|-10004|not authorized to send Apple events/i.test(t)) return "automation_denied";
400
+ if (/unconfirmed/i.test(t)) return "unconfirmed";
401
+ return "other";
402
+ }
403
+
404
+ /**
405
+ * Wrap the indexer's bridge handler so every bridged write leaves one
406
+ * timestamped line: tool, outcome, reason class, duration. The indexer log
407
+ * otherwise has no record of why an overnight send failed.
408
+ */
409
+ export function loggedBridgeHandler(run, log, now = () => Date.now()) {
410
+ return async (tool, args) => {
411
+ const started = now();
412
+ let result;
413
+ try {
414
+ result = await run(tool, args);
415
+ } catch (e) {
416
+ log(`${new Date(started).toISOString()} write bridge: ${tool} threw after ${now() - started}ms (${classifyWriteFailure(e && e.message)})`);
417
+ throw e;
418
+ }
419
+ const ok = Boolean(result && result.ok);
420
+ // Dry runs and confirm-gated previews come back ok:false with planned:true — nothing failed.
421
+ const planned = !ok && Boolean(result && result.planned);
422
+ const why = ok || planned ? "" : ` (${classifyWriteFailure(result && result.message)}${result && result.unsupported ? ", unsupported" : ""})`;
423
+ log(`${new Date(started).toISOString()} write bridge: ${tool} ${ok ? "ok" : planned ? "planned (dry run / needs confirm)" : "failed"} in ${now() - started}ms${why}`);
424
+ return result;
425
+ };
426
+ }
427
+
388
428
  export async function dispatchWriteTool(name, args = {}, deps = {}) {
389
429
  const {
390
430
  indexerMode = false,
@@ -406,16 +446,30 @@ export async function dispatchWriteTool(name, args = {}, deps = {}) {
406
446
 
407
447
  const route = planWriteRoute({ indexerMode, bridgeAvailable, toolName: name });
408
448
 
449
+ let declined = null;
409
450
  if (route.target === "daemon") {
410
451
  const { delivered, response, error } = await request({ socketPath, tool: name, args });
411
452
  const { fallbackLocal } = planAfterDelegation({ delivered, response });
412
453
  if (!fallbackLocal) {
413
454
  return response;
414
455
  }
415
- log(`Write bridge unavailable for ${name} (${error || "no response"}); running locally.`);
456
+ if (delivered && response && typeof response === "object") {
457
+ // The daemon answered but couldn't do it (e.g. it couldn't drive Mail's
458
+ // compose window). Its reason is the real one: it holds the grants.
459
+ declined = response;
460
+ log(`Write bridge declined ${name} (${classifyWriteFailure(response.message)}); running locally.`);
461
+ } else {
462
+ log(`Write bridge unavailable for ${name} (${error || "no response"}); running locally.`);
463
+ }
416
464
  }
417
465
 
418
466
  const result = runLocally(name, args);
467
+ if (declined && result && result.ok === false && declined.message) {
468
+ return {
469
+ ...result,
470
+ message: `${declined.message} (Reported by the indexer daemon, which holds the Automation/Accessibility grants. The local fallback in this process also failed: ${classifyWriteFailure(result.message)}.)`
471
+ };
472
+ }
419
473
  if (
420
474
  result &&
421
475
  result.ok === false &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-tools-mcp",
3
- "version": "3.0.4",
3
+ "version": "3.0.5",
4
4
  "description": "MCP server for semantic search and write actions across Apple Mail, Messages, Calendar, and Contacts. Speaks stdio (local) or bearer-token-authenticated Streamable HTTP (LAN / Tailscale).",
5
5
  "type": "module",
6
6
  "main": "index.js",