apple-mail-mcp 1.5.6 → 1.5.7

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/build/index.js CHANGED
@@ -24,6 +24,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
24
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
25
25
  import { z } from "zod";
26
26
  import { AppleMailManager } from "./services/appleMailManager.js";
27
+ import { createSerialGate } from "./utils/serialize.js";
27
28
  // =============================================================================
28
29
  // Shared Validation Schemas
29
30
  // =============================================================================
@@ -84,17 +85,30 @@ function errorResponse(message) {
84
85
  };
85
86
  }
86
87
  /**
87
- * Wraps a tool handler with consistent error handling.
88
+ * Serial execution gate for AppleScript-backed tool calls (issue #11).
89
+ *
90
+ * Concurrent MCP tool calls are funneled through this single gate so only one
91
+ * osascript invocation hits Mail.app's single-threaded AppleScript dispatch at
92
+ * a time, with a short settle delay between calls so the dispatch queue drains.
93
+ * Without it, a concurrent batch races into Mail.app, the later calls blow past
94
+ * their timeouts, and Mail.app is left half-recovered for the next batch.
95
+ */
96
+ const serializeAppleScript = createSerialGate();
97
+ /**
98
+ * Wraps a tool handler with consistent error handling, serialized through the
99
+ * AppleScript gate so concurrent MCP tool calls don't race into Mail.app (#11).
88
100
  */
89
101
  function withErrorHandling(handler, errorPrefix) {
90
102
  return async (params) => {
91
- try {
92
- return handler(params);
93
- }
94
- catch (error) {
95
- const message = error instanceof Error ? error.message : "Unknown error";
96
- return errorResponse(`${errorPrefix}: ${message}`);
97
- }
103
+ return serializeAppleScript(() => {
104
+ try {
105
+ return handler(params);
106
+ }
107
+ catch (error) {
108
+ const message = error instanceof Error ? error.message : "Unknown error";
109
+ return errorResponse(`${errorPrefix}: ${message}`);
110
+ }
111
+ });
98
112
  };
99
113
  }
100
114
  // =============================================================================
@@ -1 +1 @@
1
- {"version":3,"file":"applescript.d.ts","sourceRoot":"","sources":["../../src/utils/applescript.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAuOxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CA6HnB"}
1
+ {"version":3,"file":"applescript.d.ts","sourceRoot":"","sources":["../../src/utils/applescript.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAmQxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CAoInB"}
@@ -65,6 +65,32 @@ function escapeForShell(script) {
65
65
  // This is the standard shell escaping pattern for single-quoted strings
66
66
  return script.replace(/'/g, "'\\''");
67
67
  }
68
+ /**
69
+ * Headroom (ms) between the in-AppleScript `with timeout` and the outer
70
+ * osascript process timeout. The script-level timeout must fire *first* so
71
+ * Mail.app aborts the operation from inside its own AppleScript dispatch —
72
+ * cleanly releasing the event queue — before Node SIGKILLs the osascript
73
+ * process. Killing osascript alone does not stop work already dispatched into
74
+ * Mail.app, which is what wedges Mail.app for subsequent calls (issue #11).
75
+ */
76
+ const SCRIPT_TIMEOUT_HEADROOM_MS = 5000;
77
+ /**
78
+ * Wraps a script body in an AppleScript `with timeout` block so any Apple
79
+ * Event that honors timeouts (most Mail.app operations do) aborts cleanly
80
+ * rather than holding Mail.app's single-threaded AppleScript dispatch queue
81
+ * open indefinitely.
82
+ *
83
+ * The timeout is set a few seconds below the osascript process timeout so the
84
+ * in-Mail abort wins the race against the outer process kill.
85
+ *
86
+ * @param script - The AppleScript body
87
+ * @param processTimeoutMs - The outer osascript process timeout
88
+ * @returns The script wrapped in `with timeout … end timeout`
89
+ */
90
+ function wrapWithTimeout(script, processTimeoutMs) {
91
+ const seconds = Math.max(1, Math.ceil((processTimeoutMs - SCRIPT_TIMEOUT_HEADROOM_MS) / 1000));
92
+ return `with timeout of ${seconds} seconds\n${script}\nend timeout`;
93
+ }
68
94
  /**
69
95
  * Checks if an error is a timeout error from execSync.
70
96
  *
@@ -270,8 +296,10 @@ export function executeAppleScript(script, options = {}) {
270
296
  // Prepare the script:
271
297
  // 1. Trim leading/trailing whitespace (cosmetic)
272
298
  // 2. Preserve internal newlines (required for AppleScript syntax)
273
- // 3. Escape for shell execution
274
- const preparedScript = escapeForShell(script.trim());
299
+ // 3. Wrap in `with timeout` so a stuck Mail.app operation aborts from
300
+ // inside its own dispatch before the process timeout kills osascript (#11)
301
+ // 4. Escape for shell execution
302
+ const preparedScript = escapeForShell(wrapWithTimeout(script.trim(), timeoutMs));
275
303
  // Build the osascript command
276
304
  // We use single quotes to wrap the script, which is why we escape
277
305
  // single quotes within the script itself
@@ -292,6 +320,11 @@ export function executeAppleScript(script, options = {}) {
292
320
  const output = execSync(command, {
293
321
  encoding: "utf8",
294
322
  timeout: timeoutMs,
323
+ // SIGKILL (not the default SIGTERM): a wedged osascript blocked on an
324
+ // unresponsive Mail.app can ignore SIGTERM, leaking processes that pile
325
+ // up and worsen the contention. SIGKILL guarantees the process is reaped
326
+ // when the timeout fires. (#11)
327
+ killSignal: "SIGKILL",
295
328
  // Capture stderr separately to get error details
296
329
  stdio: ["pipe", "pipe", "pipe"],
297
330
  });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Serial execution gate.
3
+ *
4
+ * Mail.app's AppleScript handler is effectively single-threaded: concurrent
5
+ * `tell application "Mail"` calls queue inside Mail.app, and once enough stack
6
+ * up the later ones blow past their timeouts while earlier ones are still
7
+ * draining — the client sees a cascade of "Request timed out" errors and
8
+ * Mail.app is left half-recovered for the next batch (issue #11).
9
+ *
10
+ * A gate chains every task through a single promise so only one runs at a time,
11
+ * with an optional settle delay between tasks so Mail.app's dispatch queue can
12
+ * drain. Because tasks are chained on promises, awaiting the gate also yields
13
+ * the event loop between calls, keeping the server responsive to protocol
14
+ * traffic (pings, health-check) during a batch.
15
+ *
16
+ * @module utils/serialize
17
+ */
18
+ /** A function that serializes a task behind all previously enqueued tasks. */
19
+ export type SerialGate = <R>(task: () => R | PromiseLike<R>) => Promise<R>;
20
+ /**
21
+ * Creates a serial execution gate.
22
+ *
23
+ * @param settleMs - Delay inserted after each task before the next one starts
24
+ * (default 50ms). Set to 0 to disable.
25
+ * @returns A `serialize(task)` function. Tasks run strictly in enqueue order and
26
+ * never overlap; each call resolves/rejects with its task's own result, and a
27
+ * failing task never breaks serialization for later tasks.
28
+ */
29
+ export declare function createSerialGate(settleMs?: number): SerialGate;
30
+ //# sourceMappingURL=serialize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/utils/serialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,8EAA8E;AAC9E,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;AAE3E;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,SAAK,GAAG,UAAU,CAa1D"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Serial execution gate.
3
+ *
4
+ * Mail.app's AppleScript handler is effectively single-threaded: concurrent
5
+ * `tell application "Mail"` calls queue inside Mail.app, and once enough stack
6
+ * up the later ones blow past their timeouts while earlier ones are still
7
+ * draining — the client sees a cascade of "Request timed out" errors and
8
+ * Mail.app is left half-recovered for the next batch (issue #11).
9
+ *
10
+ * A gate chains every task through a single promise so only one runs at a time,
11
+ * with an optional settle delay between tasks so Mail.app's dispatch queue can
12
+ * drain. Because tasks are chained on promises, awaiting the gate also yields
13
+ * the event loop between calls, keeping the server responsive to protocol
14
+ * traffic (pings, health-check) during a batch.
15
+ *
16
+ * @module utils/serialize
17
+ */
18
+ /**
19
+ * Creates a serial execution gate.
20
+ *
21
+ * @param settleMs - Delay inserted after each task before the next one starts
22
+ * (default 50ms). Set to 0 to disable.
23
+ * @returns A `serialize(task)` function. Tasks run strictly in enqueue order and
24
+ * never overlap; each call resolves/rejects with its task's own result, and a
25
+ * failing task never breaks serialization for later tasks.
26
+ */
27
+ export function createSerialGate(settleMs = 50) {
28
+ let tail = Promise.resolve();
29
+ return (task) => {
30
+ const result = tail.then(task);
31
+ // Keep the chain alive regardless of this task's outcome, with a settle
32
+ // delay so the next task doesn't race straight back into Mail.app.
33
+ tail = result.then(() => settle(settleMs), () => settle(settleMs));
34
+ return result;
35
+ };
36
+ }
37
+ function settle(ms) {
38
+ if (ms <= 0)
39
+ return Promise.resolve();
40
+ return new Promise((resolve) => setTimeout(resolve, ms));
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "1.5.6",
3
+ "version": "1.5.7",
4
4
  "description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude",
5
5
  "type": "module",
6
6
  "main": "build/index.js",