apple-notes-mcp 2.10.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.
@@ -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.10.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",