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 +2 -1
- package/lib/writeTools.js +55 -1
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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",
|