apple-notes-mcp 2.11.0 → 2.12.0

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
@@ -2886,6 +2886,45 @@ Every tool that does not read the Notes database works normally without Full Dis
2886
2886
 
2887
2887
  ---
2888
2888
 
2889
+ ## Permissions check
2890
+
2891
+ `apple-notes-mcp setup --permissions` checks, in one report, the four grants that decide what the server can do on this Mac:
2892
+
2893
+ | Item | How it is checked | Needed for |
2894
+ |------|-------------------|------------|
2895
+ | Full Disk Access | one read-only `SELECT 1` against `NoteStore.sqlite` | every database-backed tool (see [Full Disk Access](#full-disk-access)) |
2896
+ | Automation of Notes.app | one read-only Apple event (the first account's name), sent only with `--probe-automation` | every AppleScript tool |
2897
+ | Shortcut bridges (optional) | `shortcuts list`, like `setup --check` | the native-write tools |
2898
+ | Speech Recognition (optional) | the public helper's `speech_status`, read without prompting | `transcribe-note-audio` before macOS 26 |
2899
+
2900
+ ```bash
2901
+ apple-notes-mcp setup --permissions # report, then press Enter to check again
2902
+ apple-notes-mcp setup --permissions --open # also open the System Settings pane of each missing grant
2903
+ apple-notes-mcp setup --permissions --once # report once and exit (also --json)
2904
+ apple-notes-mcp setup --permissions --probe-automation # also check Automation (sends one Apple event)
2905
+ ```
2906
+
2907
+ For each missing grant the report names the System Settings pane and its URL, for example `x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles` (the `com.apple.preference.security` form before macOS 13). Panes open only with `--open`, each once per run. The command never changes a setting or a grant: you make the change in System Settings, then press Enter to check again. Type `q` to stop. It exits 1 when a required grant is missing or could not be verified, and 0 otherwise, so a run that did not probe Automation exits 0 if Full Disk Access is granted.
2908
+
2909
+ Automation is the one item that cannot be read without talking to Notes.app, so it is opt-in. By default the command sends no Apple event, and the Automation line reads `unknown` (not probed) with the command that checks it. Add `--probe-automation` to send one read-only event; the first time, macOS may ask whether the app may control Notes, and **Allow** is the only way to add an Automation grant. The probe never runs under `--check` (which reports state only) or when `SSH_CONNECTION` is set, because nobody at the remote shell can answer the prompt. In those cases the line reads `unknown` even if `--probe-automation` was passed. An unprobed Automation line does not change the exit code and the report says Automation was not checked, so run the probe from a terminal on the Mac itself when you need to know. A probe that runs and fails (denied, or no answer) does exit 1.
2910
+
2911
+ macOS attributes these grants to the app that launched the process, so the report names it (for example `/Applications/iTerm.app`). Run the check from the app you use as the MCP host when you can. Claude Desktop launches servers as their own responsible process, so for it use the `doctor` tool inside Claude Desktop, and add the Node binary it names to Full Disk Access. The Speech item reads `unknown` until the [public native helper](#public-native-helper) is built.
2912
+
2913
+ ### Optional checklist window
2914
+
2915
+ The same checklist is also available in a small window with **Open Settings** and **Re-check** buttons. Like the public helper, it is built on your Mac from the packaged Swift source, never shipped as a binary:
2916
+
2917
+ ```bash
2918
+ apple-notes-mcp setup --permissions-window # compile, ad-hoc sign, verify, install
2919
+ apple-notes-mcp setup --permissions-window --check # report the installed state only
2920
+ apple-notes-mcp setup --permissions --window # open the window
2921
+ apple-notes-mcp setup --permissions --window --probe-automation # the window's checks include Automation
2922
+ ```
2923
+
2924
+ Setup compiles `native/permissions-window/apple-notes-permissions-window.swift`, signs it ad hoc, runs its `hello` handshake (which shows no window), and installs it in `~/Library/Application Support/apple-notes-mcp/permissions-window/` with a manifest of source and binary SHA-256 digests; `APPLE_NOTES_MCP_PERMISSIONS_WINDOW_DIR` overrides the folder. The window probes nothing itself. The command runs every check and sends the results to it, and opens a pane only for an item it reported. The window shows an unprobed Automation item as "Not checked", with the command that checks it, and gives it no Open Settings button. If the window is not built, is stale after an upgrade, or was modified, `--window` says so and shows the checklist in the terminal instead. The server never uses the window.
2925
+
2926
+ ---
2927
+
2889
2928
  ## Public native helper
2890
2929
 
2891
2930
  `get-note-drawings` needs Apple's PencilKit framework and `transcribe-note-audio` needs the Speech framework; neither has an AppleScript or command-line interface. For them, the server uses a small Swift helper that links public Apple frameworks only (AppKit, PencilKit, AVFoundation, and Speech). No prebuilt binary ships with the package. Build it once on your Mac:
@@ -2990,6 +3029,7 @@ In a JSON string literal the two characters `\\` denote **one** literal backslas
2990
3029
  - macOS needs automation permission
2991
3030
  - Go to System Settings > Privacy & Security > Automation
2992
3031
  - Ensure your terminal/Claude has permission to control Notes
3032
+ - `apple-notes-mcp setup --permissions --open --probe-automation` checks every grant and opens the pane of each missing one ([Permissions check](#permissions-check))
2993
3033
 
2994
3034
  ### Native writes time out or report an uncertain outcome
2995
3035
  - Symptom: `add-native-tags`, `set-note-pinned`, `append-native` or another native write fails with "Shortcuts timed out waiting for …", "Operation outcome uncertain" or "readback was not verified", while `doctor` and `get-capabilities` report the bridges installed
package/build/index.js CHANGED
@@ -24477,14 +24477,14 @@ var require_turndown_cjs = __commonJS({
24477
24477
  } else if (node.nodeType === 1) {
24478
24478
  replacement = replacementForNode.call(self, node);
24479
24479
  }
24480
- return join31(output, replacement);
24480
+ return join32(output, replacement);
24481
24481
  }, "");
24482
24482
  }
24483
24483
  function postProcess(output) {
24484
24484
  var self = this;
24485
24485
  this.rules.forEach(function(rule) {
24486
24486
  if (typeof rule.append === "function") {
24487
- output = join31(output, rule.append(self.options));
24487
+ output = join32(output, rule.append(self.options));
24488
24488
  }
24489
24489
  });
24490
24490
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -24496,7 +24496,7 @@ var require_turndown_cjs = __commonJS({
24496
24496
  if (whitespace.leading || whitespace.trailing) content = content.trim();
24497
24497
  return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
24498
24498
  }
24499
- function join31(output, replacement) {
24499
+ function join32(output, replacement) {
24500
24500
  var s1 = trimTrailingNewlines(output);
24501
24501
  var s2 = trimLeadingNewlines(replacement);
24502
24502
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -51023,13 +51023,13 @@ var SidecarWriter = class {
51023
51023
  }
51024
51024
  this.created = true;
51025
51025
  }
51026
- const safe = safeAssetName(name, mime);
51027
- const ext = extname3(safe);
51028
- const stem = safe.slice(0, safe.length - ext.length);
51026
+ const safe2 = safeAssetName(name, mime);
51027
+ const ext = extname3(safe2);
51028
+ const stem = safe2.slice(0, safe2.length - ext.length);
51029
51029
  let target;
51030
51030
  let out;
51031
51031
  for (let n = 1; n <= 1e3 && out === void 0; n++) {
51032
- target = join19(this.dir, n === 1 ? safe : `${stem}-${n}${ext}`);
51032
+ target = join19(this.dir, n === 1 ? safe2 : `${stem}-${n}${ext}`);
51033
51033
  try {
51034
51034
  out = openSync4(target, CREATE_FLAGS, 420);
51035
51035
  } catch (error2) {
@@ -51714,7 +51714,8 @@ var PUBLIC_HELPER_SETUP_COMMAND = "apple-notes-mcp setup --public-helper";
51714
51714
  var PUBLIC_HELPER_ACTIONS = /* @__PURE__ */ new Set([
51715
51715
  "hello",
51716
51716
  "decode_drawing",
51717
- "transcribe"
51717
+ "transcribe",
51718
+ "speech_status"
51718
51719
  ]);
51719
51720
  var DEFAULT_TIMEOUT_MS2 = 3e4;
51720
51721
  var MAX_OUTPUT_BYTES2 = 256 * 1024 * 1024;
@@ -52888,16 +52889,16 @@ var NOTE_PLACEHOLDERS = [
52888
52889
  "exportStem"
52889
52890
  ];
52890
52891
  var PLACEHOLDER_MODIFIERS = ["raw", "yaml"];
52891
- var wrap2 = (before, after = "", join31) => ({
52892
+ var wrap2 = (before, after = "", join32) => ({
52892
52893
  mode: "wrap",
52893
52894
  before,
52894
52895
  after,
52895
- ...join31 ? { join: join31 } : {}
52896
+ ...join32 ? { join: join32 } : {}
52896
52897
  });
52897
- var pattern = (value, join31) => ({
52898
+ var pattern = (value, join32) => ({
52898
52899
  mode: "pattern",
52899
52900
  value,
52900
- ...join31 ? { join: join31 } : {}
52901
+ ...join32 ? { join: join32 } : {}
52901
52902
  });
52902
52903
  var STANDARD = {
52903
52904
  schemaVersion: 1,
@@ -53969,12 +53970,12 @@ function pathComponent(value) {
53969
53970
  return clean || "untitled";
53970
53971
  }
53971
53972
  function templateAssetsDir(directory, outputDir, values) {
53972
- const safe = {};
53973
+ const safe2 = {};
53973
53974
  for (const [key, value] of Object.entries(values)) {
53974
53975
  const raw = typeof value === "string" ? value : value?.raw;
53975
- if (raw !== void 0) safe[key] = pathComponent(raw);
53976
+ if (raw !== void 0) safe2[key] = pathComponent(raw);
53976
53977
  }
53977
- const filled = fillPlaceholders(directory, safe);
53978
+ const filled = fillPlaceholders(directory, safe2);
53978
53979
  const base = resolve5(outputDir);
53979
53980
  const dir = resolve5(base, filled);
53980
53981
  if (isAbsolute3(filled) || filled.split("/").includes("..") || !dir.startsWith(base + sep4))
@@ -59405,6 +59406,710 @@ function formatHelperBuild(report) {
59405
59406
  return lines.join("\n");
59406
59407
  }
59407
59408
 
59409
+ // src/services/permissions.ts
59410
+ import { spawnSync as spawnSync6 } from "node:child_process";
59411
+ import { createInterface } from "node:readline";
59412
+ var PRIVACY = "System Settings > Privacy & Security";
59413
+ var PANE_ANCHORS = {
59414
+ fullDiskAccess: { anchor: "Privacy_AllFiles", label: "Full Disk Access" },
59415
+ automation: { anchor: "Privacy_Automation", label: "Automation" },
59416
+ speechRecognition: { anchor: "Privacy_SpeechRecognition", label: "Speech Recognition" }
59417
+ };
59418
+ function settingsUrl(pane, macOSVersion) {
59419
+ const legacy = macOSVersion !== null && compareVersions(macOSVersion, "13.0") < 0;
59420
+ const base = legacy ? "x-apple.systempreferences:com.apple.preference.security" : "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension";
59421
+ return `${base}?${PANE_ANCHORS[pane].anchor}`;
59422
+ }
59423
+ function paneFields(pane, macOSVersion) {
59424
+ return {
59425
+ settingsPane: `${PRIVACY} > ${PANE_ANCHORS[pane].label}`,
59426
+ settingsUrl: settingsUrl(pane, macOSVersion)
59427
+ };
59428
+ }
59429
+ function defaultCheckOptions() {
59430
+ return { probeAutomation: false, check: false, env: process.env };
59431
+ }
59432
+ function automationSkipReason(options) {
59433
+ if (!options.probeAutomation)
59434
+ return "this check sends no Apple event to Notes.app unless you pass --probe-automation";
59435
+ if (options.env.SSH_CONNECTION)
59436
+ return "this is an SSH session, where macOS cannot show its permission prompt, so no Apple event was sent";
59437
+ if (options.check)
59438
+ return "--check reports state only and never sends an Apple event to Notes.app";
59439
+ return null;
59440
+ }
59441
+ function checkPermissions(probes = defaultPermissionProbes(), options = defaultCheckOptions()) {
59442
+ const macOSVersion = safe(probes.macOSVersion, null);
59443
+ const launchingApp = safe(probes.launchingApp, null);
59444
+ const who = launchingApp ?? "the app that launches the server";
59445
+ const items = [
59446
+ fullDiskAccessItem(probes, macOSVersion),
59447
+ automationItem(probes, macOSVersion, who, automationSkipReason(options)),
59448
+ shortcutsItem(probes),
59449
+ speechItem(probes, macOSVersion, who)
59450
+ ];
59451
+ return {
59452
+ ready: items.every((item) => !item.required || item.status === "granted" || item.notProbed),
59453
+ launchingApp,
59454
+ execPath: probes.execPath,
59455
+ macOSVersion,
59456
+ items
59457
+ };
59458
+ }
59459
+ function safe(fn, fallback) {
59460
+ try {
59461
+ return fn();
59462
+ } catch {
59463
+ return fallback;
59464
+ }
59465
+ }
59466
+ function errorText(error2) {
59467
+ return error2 instanceof Error ? error2.message : String(error2);
59468
+ }
59469
+ function fullDiskAccessItem(probes, macOSVersion) {
59470
+ const base = {
59471
+ id: "fullDiskAccess",
59472
+ title: "Full Disk Access",
59473
+ required: true,
59474
+ ...paneFields("fullDiskAccess", macOSVersion)
59475
+ };
59476
+ let granted;
59477
+ try {
59478
+ granted = probes.fullDiskAccess();
59479
+ } catch (error2) {
59480
+ return {
59481
+ ...base,
59482
+ status: "unknown",
59483
+ detail: `could not probe the Notes database: ${errorText(error2)}`,
59484
+ fix: fdaRemediation(probes.execPath)
59485
+ };
59486
+ }
59487
+ return granted ? {
59488
+ ...base,
59489
+ status: "granted",
59490
+ detail: "the Notes database (NoteStore.sqlite) is readable",
59491
+ fix: null
59492
+ } : {
59493
+ ...base,
59494
+ status: "missing",
59495
+ detail: "the Notes database is not readable, so query-notes, checklist state, note metadata, note links, native objects, exports and the bridges' readback do not work",
59496
+ fix: fdaRemediation(probes.execPath)
59497
+ };
59498
+ }
59499
+ var PROBE_AUTOMATION_COMMAND = "apple-notes-mcp setup --permissions --probe-automation";
59500
+ function automationItem(probes, macOSVersion, who, skipReason) {
59501
+ const base = {
59502
+ id: "notesAutomation",
59503
+ title: "Automation of Notes.app",
59504
+ required: true,
59505
+ ...paneFields("automation", macOSVersion)
59506
+ };
59507
+ if (skipReason !== null)
59508
+ return {
59509
+ ...base,
59510
+ status: "unknown",
59511
+ detail: `not probed: ${skipReason}`,
59512
+ fix: `On this Mac, in a terminal, run \`${PROBE_AUTOMATION_COMMAND}\`. It sends one read-only Apple event to Notes.app, and macOS may ask whether this app may control Notes; choose Allow.`,
59513
+ notProbed: true
59514
+ };
59515
+ let result;
59516
+ try {
59517
+ result = probes.notesAutomation();
59518
+ } catch (error2) {
59519
+ result = { success: false, error: errorText(error2) };
59520
+ }
59521
+ if (result.success)
59522
+ return {
59523
+ ...base,
59524
+ status: "granted",
59525
+ detail: "Notes.app answered a read-only Apple event",
59526
+ fix: null
59527
+ };
59528
+ if (isPermissionDenied(result.error))
59529
+ return {
59530
+ ...base,
59531
+ status: "missing",
59532
+ detail: "macOS refused the Apple event to Notes.app (Automation denied)",
59533
+ fix: `In ${PRIVACY} > Automation, expand ${who} and turn on Notes. Then fully quit (Cmd+Q) and relaunch that app. If Notes is not listed, run this check again and choose Allow when macOS asks to control Notes.`
59534
+ };
59535
+ return {
59536
+ ...base,
59537
+ status: "unknown",
59538
+ detail: `Notes.app did not answer: ${result.error || "no response"}`,
59539
+ fix: "Open Notes.app once, then run this check again. If macOS asks whether this app may control Notes, choose Allow."
59540
+ };
59541
+ }
59542
+ function shortcutsItem(probes) {
59543
+ const base = {
59544
+ id: "shortcutBridges",
59545
+ title: "Shortcut bridges",
59546
+ required: false,
59547
+ settingsPane: null,
59548
+ settingsUrl: null
59549
+ };
59550
+ let report;
59551
+ try {
59552
+ report = probes.shortcuts();
59553
+ } catch (error2) {
59554
+ return {
59555
+ ...base,
59556
+ status: "unknown",
59557
+ detail: `could not run the shortcuts command: ${errorText(error2)}`,
59558
+ fix: "Run `apple-notes-mcp setup --check`."
59559
+ };
59560
+ }
59561
+ const required2 = report.items.filter((item) => !item.optional);
59562
+ const failed = required2.filter((item) => item.error && !item.installed);
59563
+ const missing = required2.filter((item) => !item.installed);
59564
+ const consent = "After install or upgrade, run each bridge once in Shortcuts.app and choose Always Allow.";
59565
+ if (missing.length === 0)
59566
+ return {
59567
+ ...base,
59568
+ status: "granted",
59569
+ detail: `both required bridges are installed (${required2.map((item) => item.name).join(", ")}). ${consent}`,
59570
+ fix: null
59571
+ };
59572
+ if (failed.length === missing.length && failed.length > 0)
59573
+ return {
59574
+ ...base,
59575
+ status: "unknown",
59576
+ detail: `could not inspect: ${failed.map((item) => `${item.name}: ${item.error}`).join("; ")}`,
59577
+ fix: "Run `apple-notes-mcp setup --check`."
59578
+ };
59579
+ return {
59580
+ ...base,
59581
+ status: "missing",
59582
+ detail: `not installed: ${missing.map((item) => item.name).join(", ")}. The native-write tools (append-native, checklists, tables, native tags, pinning, note links) need them; everything else works without them`,
59583
+ fix: `Run \`apple-notes-mcp setup\` and approve Add Shortcut in macOS. ${consent}`
59584
+ };
59585
+ }
59586
+ function speechItem(probes, macOSVersion, who) {
59587
+ const base = {
59588
+ id: "speechRecognition",
59589
+ title: "Speech Recognition",
59590
+ required: false,
59591
+ ...paneFields("speechRecognition", macOSVersion)
59592
+ };
59593
+ let answer;
59594
+ try {
59595
+ answer = probes.speech();
59596
+ } catch (error2) {
59597
+ answer = { ok: false, reason: errorText(error2) };
59598
+ }
59599
+ if (!answer.ok)
59600
+ return {
59601
+ ...base,
59602
+ status: "unknown",
59603
+ detail: `only transcribe-note-audio needs this; could not read the status: ${answer.reason}`,
59604
+ fix: `Build the public native helper with \`${PUBLIC_HELPER_SETUP_COMMAND}\`, then run this check again.`
59605
+ };
59606
+ const state = answer.speechAuthorization;
59607
+ if (state === "authorized")
59608
+ return {
59609
+ ...base,
59610
+ status: "granted",
59611
+ detail: "transcribe-note-audio can use on-device speech recognition",
59612
+ fix: null
59613
+ };
59614
+ if (state === "restricted")
59615
+ return {
59616
+ ...base,
59617
+ status: "missing",
59618
+ detail: "Speech Recognition is restricted on this Mac (for example by a management profile)",
59619
+ fix: "Ask the Mac's administrator; the restriction cannot be lifted in System Settings."
59620
+ };
59621
+ if (state === "denied")
59622
+ return {
59623
+ ...base,
59624
+ status: "missing",
59625
+ detail: "Speech Recognition was denied, so transcribe-note-audio stops with permission_required",
59626
+ fix: `In ${PRIVACY} > Speech Recognition, turn on ${who}, then relaunch it.`
59627
+ };
59628
+ if (!answer.requiresGrant)
59629
+ return {
59630
+ ...base,
59631
+ status: "not_needed",
59632
+ detail: "not granted, and not needed: on macOS 26 and later transcription runs on-device without a Speech Recognition grant",
59633
+ fix: null
59634
+ };
59635
+ return {
59636
+ ...base,
59637
+ status: "missing",
59638
+ detail: `status ${state}. Before macOS 26, transcription needs this grant, and the server never shows the permission prompt`,
59639
+ fix: `In ${PRIVACY} > Speech Recognition, turn on ${who} if it is listed, then relaunch it.`
59640
+ };
59641
+ }
59642
+ function findLaunchingApp(startPid = process.ppid, readProcess = readProcessEntry) {
59643
+ let pid = startPid;
59644
+ for (let depth = 0; depth < 32 && pid > 1; depth++) {
59645
+ const entry = readProcess(pid);
59646
+ if (!entry) return null;
59647
+ const match = /^(.*?\.app)\//.exec(entry.command);
59648
+ if (match) return match[1];
59649
+ pid = entry.ppid;
59650
+ }
59651
+ return null;
59652
+ }
59653
+ function readProcessEntry(pid) {
59654
+ const result = spawnSync6("/bin/ps", ["-o", "ppid=", "-o", "comm=", "-p", String(pid)], {
59655
+ encoding: "utf8",
59656
+ timeout: 3e3
59657
+ });
59658
+ const match = /^\s*(\d+)\s+(.+)$/.exec(String(result.stdout ?? "").trim());
59659
+ return match ? { ppid: Number(match[1]), command: match[2] } : null;
59660
+ }
59661
+ var AUTOMATION_TIMEOUT_MS = 6e4;
59662
+ function defaultPermissionProbes() {
59663
+ return {
59664
+ fullDiskAccess: hasFullDiskAccess,
59665
+ notesAutomation: () => {
59666
+ const result = executeAppleScript('tell application "Notes" to get name of account 1', {
59667
+ timeoutMs: AUTOMATION_TIMEOUT_MS,
59668
+ maxRetries: 1
59669
+ });
59670
+ return { success: result.success, error: result.error };
59671
+ },
59672
+ shortcuts: () => setupShortcuts(true),
59673
+ speech: () => {
59674
+ const install = inspectPublicHelper();
59675
+ if (!install.ready)
59676
+ return { ok: false, reason: install.detail ?? "the public native helper is not built" };
59677
+ const answer = callPublicHelper("speech_status");
59678
+ return {
59679
+ ok: true,
59680
+ speechAuthorization: String(answer.speechAuthorization ?? "unknown"),
59681
+ requiresGrant: answer.requiresGrant !== false
59682
+ };
59683
+ },
59684
+ macOSVersion: readMacOSVersion,
59685
+ launchingApp: () => findLaunchingApp(),
59686
+ execPath: process.execPath
59687
+ };
59688
+ }
59689
+ function openSettingsPane(item, open = openUrl) {
59690
+ if (!item.settingsUrl || !item.settingsUrl.startsWith("x-apple.systempreferences:"))
59691
+ return { ok: false, error: `${item.title} has no System Settings pane` };
59692
+ return open(item.settingsUrl);
59693
+ }
59694
+ function openUrl(url) {
59695
+ const result = spawnSync6("/usr/bin/open", [url], { encoding: "utf8" });
59696
+ return result.status === 0 ? { ok: true } : { ok: false, error: result.stderr || result.error?.message || "open failed" };
59697
+ }
59698
+ function pendingItems(report) {
59699
+ return report.items.filter(
59700
+ (item) => !item.notProbed && (item.status === "missing" || item.status === "unknown")
59701
+ );
59702
+ }
59703
+ var ICONS = {
59704
+ granted: "\u2713",
59705
+ not_needed: "\u2713",
59706
+ missing: "\u2717",
59707
+ unknown: "?"
59708
+ };
59709
+ function formatPermissionsReport(report) {
59710
+ const lines = ["Apple Notes MCP permissions", ""];
59711
+ lines.push(
59712
+ report.launchingApp ? `macOS attributes these grants to ${report.launchingApp}, the app that launched this check.` : `No launching app was found above this process, so macOS attributes these grants to ${report.execPath}.`
59713
+ );
59714
+ lines.push(
59715
+ "An MCP host (Claude Desktop, an editor) can hold different grants: run this check from the same app, or use the doctor tool there.",
59716
+ ""
59717
+ );
59718
+ for (const item of report.items) {
59719
+ const tag = item.required ? "" : " (optional)";
59720
+ lines.push(`${ICONS[item.status]} ${item.title}${tag}: ${item.detail}`);
59721
+ if (item.notProbed) {
59722
+ if (item.fix) lines.push(` To check: ${item.fix}`);
59723
+ } else if (item.status === "missing" || item.status === "unknown") {
59724
+ if (item.settingsPane) lines.push(` Pane: ${item.settingsPane}`);
59725
+ if (item.settingsUrl) lines.push(` URL: ${item.settingsUrl}`);
59726
+ if (item.fix) lines.push(` Fix: ${item.fix}`);
59727
+ }
59728
+ }
59729
+ lines.push("");
59730
+ const unchecked = report.items.some((item) => item.notProbed);
59731
+ lines.push(
59732
+ !report.ready ? "Required permissions are missing; the server works only partly until they are granted." : unchecked ? "Every required permission that was checked is granted. Automation of Notes.app was not checked." : "Every required permission is granted."
59733
+ );
59734
+ return lines.join("\n");
59735
+ }
59736
+ function parsePermissionsArgs(args) {
59737
+ return {
59738
+ open: args.includes("--open"),
59739
+ once: args.includes("--once") || args.includes("--check"),
59740
+ json: args.includes("--json"),
59741
+ probeAutomation: args.includes("--probe-automation"),
59742
+ check: args.includes("--check")
59743
+ };
59744
+ }
59745
+ async function runPermissionsCli(options, deps) {
59746
+ const opened = /* @__PURE__ */ new Set();
59747
+ for (; ; ) {
59748
+ const report = deps.check();
59749
+ deps.write(
59750
+ options.json ? JSON.stringify(report, null, 2) + "\n" : formatPermissionsReport(report) + "\n"
59751
+ );
59752
+ const pending = pendingItems(report);
59753
+ if (options.open) {
59754
+ for (const item of pending) {
59755
+ if (!item.settingsUrl || opened.has(item.id)) continue;
59756
+ opened.add(item.id);
59757
+ const result = deps.open(item);
59758
+ if (!options.json)
59759
+ deps.write(
59760
+ result.ok ? `Opened ${item.settingsPane}.
59761
+ ` : `Could not open ${item.settingsPane}: ${result.error}
59762
+ `
59763
+ );
59764
+ }
59765
+ }
59766
+ if (pending.length === 0 || options.once || !deps.interactive) return report.ready ? 0 : 1;
59767
+ deps.write("\nPress Enter to check again, or type q and press Enter to quit. ");
59768
+ const line = await deps.waitForEnter();
59769
+ if (line === null || line.trim().toLowerCase() === "q") return report.ready ? 0 : 1;
59770
+ deps.write("\n");
59771
+ }
59772
+ }
59773
+ function defaultPermissionsCliDeps(options) {
59774
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
59775
+ const rl = interactive ? createInterface({ input: process.stdin }) : null;
59776
+ const lines = [];
59777
+ const waiters = [];
59778
+ let closed = false;
59779
+ rl?.on("line", (line) => {
59780
+ const waiter = waiters.shift();
59781
+ if (waiter) waiter(line);
59782
+ else lines.push(line);
59783
+ });
59784
+ rl?.on("close", () => {
59785
+ closed = true;
59786
+ for (const waiter of waiters.splice(0)) waiter(null);
59787
+ });
59788
+ return {
59789
+ check: () => checkPermissions(defaultPermissionProbes(), {
59790
+ probeAutomation: options.probeAutomation,
59791
+ check: options.check,
59792
+ env: process.env
59793
+ }),
59794
+ open: (item) => openSettingsPane(item),
59795
+ write: (text2) => process.stdout.write(text2),
59796
+ waitForEnter: () => new Promise((resolveLine) => {
59797
+ if (lines.length) resolveLine(lines.shift());
59798
+ else if (closed || !rl) resolveLine(null);
59799
+ else waiters.push(resolveLine);
59800
+ }),
59801
+ interactive,
59802
+ close: () => rl?.close()
59803
+ };
59804
+ }
59805
+
59806
+ // src/services/permissionsWindow.ts
59807
+ import { spawn as spawn2, spawnSync as spawnSync7 } from "node:child_process";
59808
+ import {
59809
+ chmodSync as chmodSync3,
59810
+ existsSync as existsSync17,
59811
+ mkdirSync as mkdirSync10,
59812
+ mkdtempSync as mkdtempSync9,
59813
+ readFileSync as readFileSync6,
59814
+ renameSync as renameSync4,
59815
+ rmSync as rmSync9,
59816
+ writeFileSync as writeFileSync8
59817
+ } from "node:fs";
59818
+ import { homedir as homedir22 } from "node:os";
59819
+ import { join as join31 } from "node:path";
59820
+ import { createInterface as createInterface2 } from "node:readline";
59821
+ var PERMISSIONS_WINDOW_PROTOCOL = 1;
59822
+ var PERMISSIONS_WINDOW_DIR_ENV = "APPLE_NOTES_MCP_PERMISSIONS_WINDOW_DIR";
59823
+ var PERMISSIONS_WINDOW_BINARY = "apple-notes-permissions-window";
59824
+ var PERMISSIONS_WINDOW_SOURCE = "native/permissions-window/apple-notes-permissions-window.swift";
59825
+ var PERMISSIONS_WINDOW_MANIFEST = "manifest.json";
59826
+ var PERMISSIONS_WINDOW_SETUP_COMMAND = "apple-notes-mcp setup --permissions-window";
59827
+ var PERMISSIONS_WINDOW_BUNDLE_ID = "apple-notes-mcp.permissions-window";
59828
+ var windowManifestSchema = external_exports.object({
59829
+ schemaVersion: external_exports.literal(1),
59830
+ protocolVersion: external_exports.number().int(),
59831
+ sourceSha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
59832
+ binarySha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
59833
+ builtAt: external_exports.string(),
59834
+ compiler: external_exports.string()
59835
+ });
59836
+ function defaultPermissionsWindowDeps(overrides = {}) {
59837
+ return {
59838
+ env: process.env,
59839
+ platform: process.platform,
59840
+ sourcePath: join31(packageRoot(), PERMISSIONS_WINDOW_SOURCE),
59841
+ exists: existsSync17,
59842
+ readFile: (path10) => readFileSync6(path10),
59843
+ spawn: spawnSync7,
59844
+ now: () => /* @__PURE__ */ new Date(),
59845
+ ...overrides
59846
+ };
59847
+ }
59848
+ function permissionsWindowInstallDir(env = process.env) {
59849
+ const override = env[PERMISSIONS_WINDOW_DIR_ENV]?.trim();
59850
+ if (override) return override;
59851
+ return join31(homedir22(), "Library", "Application Support", "apple-notes-mcp", "permissions-window");
59852
+ }
59853
+ function inspectPermissionsWindow(deps = defaultPermissionsWindowDeps()) {
59854
+ const installDir = permissionsWindowInstallDir(deps.env);
59855
+ const binaryPath = join31(installDir, PERMISSIONS_WINDOW_BINARY);
59856
+ const fail = (reason, detail) => ({ ready: false, reason, detail, installDir, binaryPath });
59857
+ if (deps.platform !== "darwin") return fail("unsupported_platform", "macOS only");
59858
+ const rebuild = `Run \`${PERMISSIONS_WINDOW_SETUP_COMMAND}\`.`;
59859
+ if (!deps.exists(deps.sourcePath))
59860
+ return fail("window_not_installed", `Packaged window source is missing: ${deps.sourcePath}`);
59861
+ const manifestPath = join31(installDir, PERMISSIONS_WINDOW_MANIFEST);
59862
+ if (!deps.exists(binaryPath) || !deps.exists(manifestPath))
59863
+ return fail("window_not_installed", `The permissions window is not built. ${rebuild}`);
59864
+ let manifest;
59865
+ try {
59866
+ manifest = windowManifestSchema.parse(JSON.parse(deps.readFile(manifestPath).toString("utf8")));
59867
+ } catch {
59868
+ return fail("window_manifest_invalid", `The window manifest is unreadable. ${rebuild}`);
59869
+ }
59870
+ if (manifest.sourceSha256 !== sha256Hex(deps.readFile(deps.sourcePath)) || manifest.protocolVersion !== PERMISSIONS_WINDOW_PROTOCOL)
59871
+ return fail(
59872
+ "window_stale",
59873
+ `The installed window was built from a different source than this version ships. ${rebuild}`
59874
+ );
59875
+ if (sha256Hex(deps.readFile(binaryPath)) !== manifest.binarySha256)
59876
+ return fail(
59877
+ "window_modified",
59878
+ `The window binary no longer matches the checksum recorded when it was built. ${rebuild}`
59879
+ );
59880
+ return { ready: true, reason: null, detail: null, installDir, binaryPath };
59881
+ }
59882
+ function permissionsWindowInfoPlist() {
59883
+ return [
59884
+ '<?xml version="1.0" encoding="UTF-8"?>',
59885
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
59886
+ '<plist version="1.0">',
59887
+ "<dict>",
59888
+ " <key>CFBundleIdentifier</key>",
59889
+ ` <string>${PERMISSIONS_WINDOW_BUNDLE_ID}</string>`,
59890
+ " <key>CFBundleName</key>",
59891
+ " <string>Apple Notes MCP Permissions</string>",
59892
+ " <key>CFBundleInfoDictionaryVersion</key>",
59893
+ " <string>6.0</string>",
59894
+ "</dict>",
59895
+ "</plist>",
59896
+ ""
59897
+ ].join("\n");
59898
+ }
59899
+ function permissionsWindowCompileArguments(sourcePath, digestPath, plistPath, outputPath) {
59900
+ return [
59901
+ "swiftc",
59902
+ "-O",
59903
+ "-parse-as-library",
59904
+ "-framework",
59905
+ "AppKit",
59906
+ "-framework",
59907
+ "SwiftUI",
59908
+ "-Xlinker",
59909
+ "-sectcreate",
59910
+ "-Xlinker",
59911
+ "__TEXT",
59912
+ "-Xlinker",
59913
+ "__info_plist",
59914
+ "-Xlinker",
59915
+ plistPath,
59916
+ sourcePath,
59917
+ digestPath,
59918
+ "-o",
59919
+ outputPath
59920
+ ];
59921
+ }
59922
+ var helloSchema2 = external_exports.object({
59923
+ type: external_exports.literal("hello"),
59924
+ protocolVersion: external_exports.number().int(),
59925
+ sourceSha256: external_exports.string()
59926
+ });
59927
+ function buildPermissionsWindow(checkOnly, deps = defaultPermissionsWindowDeps()) {
59928
+ const steps = [];
59929
+ const finish = () => {
59930
+ const installation2 = inspectPermissionsWindow(deps);
59931
+ return {
59932
+ ok: installation2.ready && steps.every((s) => s.ok),
59933
+ checkOnly,
59934
+ steps,
59935
+ installation: installation2
59936
+ };
59937
+ };
59938
+ if (checkOnly) {
59939
+ const installation2 = inspectPermissionsWindow(deps);
59940
+ steps.push({
59941
+ step: "inspect installed window",
59942
+ ok: installation2.ready,
59943
+ detail: installation2.ready ? installation2.binaryPath : installation2.detail ?? void 0
59944
+ });
59945
+ return finish();
59946
+ }
59947
+ if (deps.platform !== "darwin") {
59948
+ steps.push({ step: "platform", ok: false, detail: "macOS only" });
59949
+ return finish();
59950
+ }
59951
+ if (!deps.exists(deps.sourcePath)) {
59952
+ steps.push({ step: "locate source", ok: false, detail: deps.sourcePath });
59953
+ return finish();
59954
+ }
59955
+ const sourceSha = sha256Hex(deps.readFile(deps.sourcePath));
59956
+ steps.push({ step: "locate source", ok: true, detail: `sha256 ${sourceSha}` });
59957
+ const version3 = deps.spawn("/usr/bin/xcrun", ["swiftc", "--version"], { encoding: "utf8" });
59958
+ if (version3.status !== 0) {
59959
+ steps.push({
59960
+ step: "find compiler",
59961
+ ok: false,
59962
+ detail: "No Swift compiler found. Install the Command Line Tools with `xcode-select --install`."
59963
+ });
59964
+ return finish();
59965
+ }
59966
+ const compiler = String(version3.stdout || version3.stderr || "").split("\n").find((line) => line.includes("Swift version"))?.trim() || "swiftc";
59967
+ steps.push({ step: "find compiler", ok: true, detail: compiler });
59968
+ const installDir = permissionsWindowInstallDir(deps.env);
59969
+ mkdirSync10(installDir, { recursive: true, mode: 448 });
59970
+ const staging = mkdtempSync9(join31(installDir, ".staging-"));
59971
+ try {
59972
+ const stagedBinary = join31(staging, PERMISSIONS_WINDOW_BINARY);
59973
+ const digestPath = join31(staging, "source-digest.swift");
59974
+ const plistPath = join31(staging, "Info.plist");
59975
+ writeFileSync8(digestPath, sourceDigestSwift(sourceSha), { mode: 384 });
59976
+ writeFileSync8(plistPath, permissionsWindowInfoPlist(), { mode: 384 });
59977
+ const compile = deps.spawn(
59978
+ "/usr/bin/xcrun",
59979
+ permissionsWindowCompileArguments(deps.sourcePath, digestPath, plistPath, stagedBinary),
59980
+ { encoding: "utf8", timeout: 3e5 }
59981
+ );
59982
+ if (compile.status !== 0) {
59983
+ steps.push({
59984
+ step: "compile",
59985
+ ok: false,
59986
+ detail: String(compile.stderr || compile.error?.message || "swiftc failed").slice(0, 4e3)
59987
+ });
59988
+ return finish();
59989
+ }
59990
+ steps.push({ step: "compile", ok: true });
59991
+ const sign = deps.spawn(
59992
+ "/usr/bin/codesign",
59993
+ ["--force", "--sign", "-", "--identifier", PERMISSIONS_WINDOW_BUNDLE_ID, stagedBinary],
59994
+ { encoding: "utf8" }
59995
+ );
59996
+ if (sign.status !== 0) {
59997
+ steps.push({ step: "ad-hoc sign", ok: false, detail: String(sign.stderr || "failed") });
59998
+ return finish();
59999
+ }
60000
+ steps.push({ step: "ad-hoc sign", ok: true });
60001
+ const hello = deps.spawn(stagedBinary, [], {
60002
+ input: JSON.stringify({ type: "hello" }) + "\n",
60003
+ encoding: "utf8",
60004
+ timeout: 1e4,
60005
+ killSignal: "SIGKILL"
60006
+ });
60007
+ let parsed = null;
60008
+ try {
60009
+ parsed = helloSchema2.parse(JSON.parse(String(hello.stdout ?? "").trim()));
60010
+ } catch {
60011
+ parsed = null;
60012
+ }
60013
+ if (!parsed || parsed.protocolVersion !== PERMISSIONS_WINDOW_PROTOCOL || parsed.sourceSha256 !== sourceSha) {
60014
+ steps.push({
60015
+ step: "handshake",
60016
+ ok: false,
60017
+ detail: parsed ? `window reported protocol ${parsed.protocolVersion}, source ${parsed.sourceSha256}` : `no valid hello (exit ${hello.status})`
60018
+ });
60019
+ return finish();
60020
+ }
60021
+ steps.push({ step: "handshake", ok: true });
60022
+ const manifest = {
60023
+ schemaVersion: 1,
60024
+ protocolVersion: parsed.protocolVersion,
60025
+ sourceSha256: sourceSha,
60026
+ binarySha256: sha256Hex(deps.readFile(stagedBinary)),
60027
+ builtAt: deps.now().toISOString(),
60028
+ compiler
60029
+ };
60030
+ chmodSync3(stagedBinary, 448);
60031
+ renameSync4(stagedBinary, join31(installDir, PERMISSIONS_WINDOW_BINARY));
60032
+ writeFileSync8(
60033
+ join31(installDir, PERMISSIONS_WINDOW_MANIFEST),
60034
+ JSON.stringify(manifest, null, 2) + "\n",
60035
+ { mode: 384 }
60036
+ );
60037
+ steps.push({ step: "install", ok: true, detail: installDir });
60038
+ } finally {
60039
+ rmSync9(staging, { recursive: true, force: true });
60040
+ }
60041
+ const installation = inspectPermissionsWindow(deps);
60042
+ steps.push({
60043
+ step: "verify installation",
60044
+ ok: installation.ready,
60045
+ detail: installation.ready ? void 0 : installation.detail ?? void 0
60046
+ });
60047
+ return finish();
60048
+ }
60049
+ function formatPermissionsWindowBuild(report) {
60050
+ const lines = ["Apple Notes MCP permissions window", ""];
60051
+ for (const step of report.steps)
60052
+ lines.push(`${step.ok ? "\u2713" : "\u2717"} ${step.step}${step.detail ? `: ${step.detail}` : ""}`);
60053
+ lines.push("");
60054
+ if (report.ok)
60055
+ lines.push(
60056
+ `Installed at ${report.installation.binaryPath}. Open it with \`apple-notes-mcp setup --permissions --window\`.`
60057
+ );
60058
+ else if (report.checkOnly) lines.push(`Run \`${PERMISSIONS_WINDOW_SETUP_COMMAND}\` to build it.`);
60059
+ else lines.push("The window was not installed. Fix the failed step above and run setup again.");
60060
+ return lines.join("\n");
60061
+ }
60062
+ var windowMessageSchema = external_exports.discriminatedUnion("type", [
60063
+ external_exports.object({ type: external_exports.literal("open"), id: external_exports.string().max(64) }),
60064
+ external_exports.object({ type: external_exports.literal("recheck") })
60065
+ ]);
60066
+ function runPermissionsWindow(binaryPath, session) {
60067
+ const launch = session.launch ?? ((path10) => spawn2(path10, [], { stdio: ["pipe", "pipe", "inherit"] }));
60068
+ const log = session.log ?? (() => {
60069
+ });
60070
+ let report = session.check();
60071
+ const child2 = launch(binaryPath);
60072
+ const sendReport = () => {
60073
+ if (child2.stdin && !child2.stdin.destroyed)
60074
+ child2.stdin.write(JSON.stringify({ type: "report", report }) + "\n");
60075
+ };
60076
+ child2.stdin?.on("error", () => {
60077
+ });
60078
+ sendReport();
60079
+ if (child2.stdout) {
60080
+ const lines = createInterface2({ input: child2.stdout });
60081
+ lines.on("line", (line) => {
60082
+ let message;
60083
+ try {
60084
+ message = windowMessageSchema.parse(JSON.parse(line));
60085
+ } catch {
60086
+ return;
60087
+ }
60088
+ if (message.type === "recheck") {
60089
+ report = session.check();
60090
+ sendReport();
60091
+ return;
60092
+ }
60093
+ const item = report.items.find((candidate) => candidate.id === message.id);
60094
+ if (!item || item.notProbed) return;
60095
+ const result = session.open(item);
60096
+ log(
60097
+ result.ok ? `Opened ${item.settingsPane}.
60098
+ ` : `Could not open ${item.settingsPane ?? item.title}: ${result.error}
60099
+ `
60100
+ );
60101
+ });
60102
+ }
60103
+ return new Promise((resolveExit) => {
60104
+ child2.on("error", (error2) => {
60105
+ log(`Could not run the permissions window: ${error2.message}
60106
+ `);
60107
+ resolveExit(report.ready ? 0 : 1);
60108
+ });
60109
+ child2.on("close", () => resolveExit(report.ready ? 0 : 1));
60110
+ });
60111
+ }
60112
+
59408
60113
  // src/tools/privateHelperTools.ts
59409
60114
  var coreDataId2 = external_exports.string().regex(/^x-coredata:\/\/[0-9A-F-]+\/ICNote\/p\d+$/i);
59410
60115
  var notesUuid = external_exports.string().regex(UUID_PATTERN);
@@ -59528,6 +60233,32 @@ if (process.argv[2] === "setup" && process.argv.slice(3).includes("--native-help
59528
60233
  process.stdout.write(formatHelperBuild(report) + "\n");
59529
60234
  process.exit(report.ok ? 0 : 1);
59530
60235
  }
60236
+ if (process.argv[2] === "setup" && process.argv.slice(3).includes("--permissions-window")) {
60237
+ const report = buildPermissionsWindow(process.argv.slice(3).includes("--check"));
60238
+ process.stdout.write(formatPermissionsWindowBuild(report) + "\n");
60239
+ process.exit(report.ok ? 0 : 1);
60240
+ }
60241
+ if (process.argv[2] === "setup" && process.argv.slice(3).includes("--permissions")) {
60242
+ const args = process.argv.slice(3);
60243
+ const options = parsePermissionsArgs(args);
60244
+ const cli = defaultPermissionsCliDeps(options);
60245
+ let code;
60246
+ const window2 = args.includes("--window") ? inspectPermissionsWindow() : null;
60247
+ if (window2?.ready) {
60248
+ code = await runPermissionsWindow(window2.binaryPath, {
60249
+ check: cli.check,
60250
+ open: (item) => openSettingsPane(item),
60251
+ log: cli.write
60252
+ });
60253
+ } else {
60254
+ if (window2) cli.write(`${window2.detail} Showing the checklist here instead.
60255
+
60256
+ `);
60257
+ code = await runPermissionsCli(options, cli);
60258
+ }
60259
+ cli.close();
60260
+ process.exit(code);
60261
+ }
59531
60262
  if (process.argv[2] === "setup") {
59532
60263
  const report = setupShortcuts(process.argv.slice(3).includes("--check"));
59533
60264
  process.stdout.write(formatShortcutSetup(report) + "\n");
@@ -0,0 +1,215 @@
1
+ // apple-notes-permissions-window
2
+ //
3
+ // An optional setup window for apple-notes-mcp. It shows the permissions
4
+ // checklist that `apple-notes-mcp setup --permissions` prints, with an
5
+ // "Open Settings" button per missing grant and a "Re-check" button. It probes
6
+ // nothing itself: the TypeScript side runs every check and sends the results,
7
+ // so the window and the terminal report always agree. It never reads the Notes
8
+ // database, never sends Notes an Apple event, and never changes a setting.
9
+ //
10
+ // Protocol: JSON lines on stdin and stdout.
11
+ // in: {"type": "hello"} answer the handshake and exit, no UI
12
+ // in: {"type": "report", "report": {...}} show or refresh the checklist
13
+ // out: {"type": "hello", "protocolVersion": 1, "sourceSha256": "..."}
14
+ // out: {"type": "open", "id": "<item id>"} the user asked to open that item's pane
15
+ // out: {"type": "recheck"} the user asked for a fresh check
16
+ // The window closes when stdin ends; closing the window ends the process.
17
+ //
18
+ // Build (done by `apple-notes-mcp setup --permissions-window`, which also
19
+ // generates the one-line source-digest file that defines helperSourceSHA256
20
+ // and the Info.plist linked into __TEXT,__info_plist):
21
+ // xcrun swiftc -O -parse-as-library -framework AppKit -framework SwiftUI \
22
+ // -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker Info.plist \
23
+ // apple-notes-permissions-window.swift source-digest.swift -o apple-notes-permissions-window
24
+
25
+ import AppKit
26
+ import Foundation
27
+ import SwiftUI
28
+
29
+ let protocolVersion = 1
30
+
31
+ func send(_ object: [String: Any]) {
32
+ guard let data = try? JSONSerialization.data(withJSONObject: object, options: [.sortedKeys]) else { return }
33
+ FileHandle.standardOutput.write(data)
34
+ FileHandle.standardOutput.write(Data([0x0A]))
35
+ }
36
+
37
+ struct Item: Identifiable {
38
+ let id: String
39
+ let title: String
40
+ let status: String
41
+ let required: Bool
42
+ let detail: String
43
+ let settingsPane: String?
44
+ let hasSettings: Bool
45
+ let fix: String?
46
+ /// The command deliberately did not probe this item (Automation is opt-in).
47
+ let notProbed: Bool
48
+
49
+ init?(_ object: [String: Any]) {
50
+ guard let id = object["id"] as? String, let title = object["title"] as? String,
51
+ let status = object["status"] as? String else { return nil }
52
+ self.id = id
53
+ self.title = title
54
+ self.status = status
55
+ required = object["required"] as? Bool ?? false
56
+ detail = object["detail"] as? String ?? ""
57
+ settingsPane = object["settingsPane"] as? String
58
+ hasSettings = (object["settingsUrl"] as? String)?.hasPrefix("x-apple.systempreferences:") ?? false
59
+ fix = object["fix"] as? String
60
+ notProbed = object["notProbed"] as? Bool ?? false
61
+ }
62
+
63
+ var pending: Bool { !notProbed && (status == "missing" || status == "unknown") }
64
+ }
65
+
66
+ final class Model: ObservableObject {
67
+ @Published var items: [Item] = []
68
+ @Published var ready = false
69
+ @Published var launchingApp: String?
70
+ @Published var checking = false
71
+
72
+ func apply(_ report: [String: Any]) {
73
+ items = (report["items"] as? [[String: Any]] ?? []).compactMap(Item.init)
74
+ ready = report["ready"] as? Bool ?? false
75
+ launchingApp = report["launchingApp"] as? String
76
+ checking = false
77
+ }
78
+
79
+ func recheck() {
80
+ checking = true
81
+ send(["type": "recheck"])
82
+ }
83
+
84
+ func open(_ item: Item) {
85
+ send(["type": "open", "id": item.id])
86
+ }
87
+ }
88
+
89
+ struct StatusIcon: View {
90
+ let status: String
91
+ var body: some View {
92
+ switch status {
93
+ case "granted", "not_needed":
94
+ Image(systemName: "checkmark.circle.fill").foregroundStyle(.green)
95
+ case "missing":
96
+ Image(systemName: "xmark.circle.fill").foregroundStyle(.red)
97
+ case "not_checked":
98
+ Image(systemName: "minus.circle.fill").foregroundStyle(.secondary)
99
+ default:
100
+ Image(systemName: "questionmark.circle.fill").foregroundStyle(.orange)
101
+ }
102
+ }
103
+ }
104
+
105
+ struct ChecklistView: View {
106
+ @ObservedObject var model: Model
107
+
108
+ var body: some View {
109
+ VStack(alignment: .leading, spacing: 14) {
110
+ Text("Apple Notes MCP permissions").font(.title2).bold()
111
+ Text(model.launchingApp.map { "Grants shown are for \($0), the app that launched this check." }
112
+ ?? "Grants shown are for the process that launched this check.")
113
+ .font(.callout).foregroundStyle(.secondary)
114
+ ForEach(model.items) { item in
115
+ HStack(alignment: .top, spacing: 10) {
116
+ StatusIcon(status: item.notProbed ? "not_checked" : item.status).font(.title3)
117
+ VStack(alignment: .leading, spacing: 4) {
118
+ Text(item.required ? item.title : "\(item.title) (optional)").font(.headline)
119
+ if item.notProbed {
120
+ Text("Not checked").font(.caption).bold().foregroundStyle(.secondary)
121
+ }
122
+ Text(item.detail).font(.callout).fixedSize(horizontal: false, vertical: true)
123
+ if item.pending || item.notProbed, let fix = item.fix {
124
+ Text(fix).font(.caption).foregroundStyle(.secondary)
125
+ .fixedSize(horizontal: false, vertical: true)
126
+ .textSelection(.enabled)
127
+ }
128
+ }
129
+ Spacer(minLength: 8)
130
+ if item.pending && item.hasSettings {
131
+ Button("Open Settings") { model.open(item) }
132
+ .help(item.settingsPane ?? "")
133
+ }
134
+ }
135
+ }
136
+ Divider()
137
+ HStack {
138
+ Text(!model.ready ? "Required permissions are missing."
139
+ : model.items.contains { $0.notProbed } ? "Every required permission that was checked is granted."
140
+ : "Every required permission is granted.")
141
+ .font(.callout)
142
+ Spacer()
143
+ if model.checking { ProgressView().controlSize(.small) }
144
+ Button("Re-check") { model.recheck() }
145
+ .keyboardShortcut("r")
146
+ .disabled(model.checking)
147
+ }
148
+ }
149
+ .padding(20)
150
+ .frame(width: 620)
151
+ }
152
+ }
153
+
154
+ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
155
+ let model = Model()
156
+ var window: NSWindow?
157
+
158
+ func applicationDidFinishLaunching(_ notification: Notification) {
159
+ let window = NSWindow(
160
+ contentRect: NSRect(x: 0, y: 0, width: 620, height: 420),
161
+ styleMask: [.titled, .closable, .miniaturizable],
162
+ backing: .buffered,
163
+ defer: false
164
+ )
165
+ window.title = "Apple Notes MCP Permissions"
166
+ window.contentView = NSHostingView(rootView: ChecklistView(model: model))
167
+ window.delegate = self
168
+ window.center()
169
+ window.makeKeyAndOrderFront(nil)
170
+ self.window = window
171
+ NSApp.activate(ignoringOtherApps: true)
172
+ }
173
+
174
+ func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
175
+ }
176
+
177
+ /// Reads JSON lines from stdin on a background thread and hands reports to the UI.
178
+ func readReports(into delegate: AppDelegate) {
179
+ Thread.detachNewThread {
180
+ while let line = readLine(strippingNewline: true) {
181
+ guard let data = line.data(using: .utf8),
182
+ let message = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
183
+ message["type"] as? String == "report",
184
+ let report = message["report"] as? [String: Any] else { continue }
185
+ DispatchQueue.main.async { delegate.model.apply(report) }
186
+ }
187
+ // The server side went away: nothing can re-check any more.
188
+ DispatchQueue.main.async { NSApp.terminate(nil) }
189
+ }
190
+ }
191
+
192
+ @main
193
+ struct PermissionsWindow {
194
+ static func main() {
195
+ guard let first = readLine(strippingNewline: true),
196
+ let data = first.data(using: .utf8),
197
+ let message = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
198
+ exit(2)
199
+ }
200
+ if message["type"] as? String == "hello" {
201
+ send(["type": "hello", "protocolVersion": protocolVersion, "sourceSha256": helperSourceSHA256])
202
+ exit(0)
203
+ }
204
+ guard message["type"] as? String == "report", let report = message["report"] as? [String: Any] else {
205
+ exit(2)
206
+ }
207
+ let app = NSApplication.shared
208
+ let delegate = AppDelegate()
209
+ app.delegate = delegate
210
+ app.setActivationPolicy(.regular)
211
+ delegate.model.apply(report)
212
+ readReports(into: delegate)
213
+ app.run()
214
+ }
215
+ }
@@ -17,6 +17,8 @@
17
17
  // encode_drawing strokes -> PencilKit drawing bytes (base64); used to build
18
18
  // synthetic test fixtures, never to write to Notes
19
19
  // transcribe on-device Speech transcription of one audio file
20
+ // speech_status the Speech Recognition authorization status, read without
21
+ // prompting, and whether transcription would need a grant
20
22
  //
21
23
  // Build (done by `apple-notes-mcp setup --public-helper`, which also generates
22
24
  // the one-line source-digest file that defines helperSourceSHA256 and the
@@ -613,9 +615,22 @@ func transcribe(_ request: [String: Any]) async throws -> [String: Any] {
613
615
  return response
614
616
  }
615
617
 
618
+ /// Reports what `transcribe` would find, without asking for access. On
619
+ /// macOS 26+ SpeechAnalyzer needs no grant, so only an explicit refusal
620
+ /// blocks it; earlier releases use the legacy recognizer, which needs one.
621
+ func speechStatus() -> [String: Any] {
622
+ var requiresGrant = true
623
+ if #available(macOS 26, *) { requiresGrant = false }
624
+ return [
625
+ "status": "ok",
626
+ "speechAuthorization": authorizationName(SFSpeechRecognizer.authorizationStatus()),
627
+ "requiresGrant": requiresGrant,
628
+ ]
629
+ }
630
+
616
631
  // MARK: - Dispatch
617
632
 
618
- let actions = ["hello", "decode_drawing", "encode_drawing", "transcribe"]
633
+ let actions = ["hello", "decode_drawing", "encode_drawing", "transcribe", "speech_status"]
619
634
 
620
635
  func handle(_ request: [String: Any]) async throws -> [String: Any] {
621
636
  guard (request["protocol"] as? NSNumber)?.intValue == protocolVersion else {
@@ -635,6 +650,8 @@ func handle(_ request: [String: Any]) async throws -> [String: Any] {
635
650
  return try encodeDrawing(request)
636
651
  case "transcribe":
637
652
  return try await transcribe(request)
653
+ case "speech_status":
654
+ return speechStatus()
638
655
  default:
639
656
  throw HelperFailure(code: "unknown_action", message: "unknown action")
640
657
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "MCP server for Apple Notes - read, search, create, edit, organize, and export notes on macOS via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",