apple-notes-mcp 2.9.5 → 2.9.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.
@@ -0,0 +1,257 @@
1
+ // apple-notes-public-helper
2
+ //
3
+ // A small command-line helper for apple-notes-mcp that uses PUBLIC Apple
4
+ // frameworks only. It never opens the Notes database and never writes to the
5
+ // Notes group container: the TypeScript server reads the bytes it needs
6
+ // (read-only) and hands them over on stdin.
7
+ //
8
+ // Protocol: one JSON object on stdin, one JSON object on stdout.
9
+ // request: {"protocol": 1, "action": "<name>", ...fields}
10
+ // success: {"status": "ok", ...}
11
+ // failure: {"status": "error", "code": "<code>", "message": "<text>"}
12
+ //
13
+ // Actions:
14
+ // hello protocol version, embedded source digest, action list
15
+ // decode_drawing PencilKit drawing bytes (base64) -> strokes
16
+ // encode_drawing strokes -> PencilKit drawing bytes (base64); used to build
17
+ // synthetic test fixtures, never to write to Notes
18
+ //
19
+ // Build (done by `apple-notes-mcp setup --public-helper`, which also generates
20
+ // the one-line source-digest file that defines helperSourceSHA256 and the
21
+ // Info.plist linked into __TEXT,__info_plist; PencilKit needs a bundle id):
22
+ // xcrun swiftc -O -parse-as-library -framework AppKit -framework PencilKit \
23
+ // -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker Info.plist \
24
+ // apple-notes-public-helper.swift source-digest.swift -o apple-notes-public-helper
25
+
26
+ import AppKit
27
+ import Foundation
28
+ import PencilKit
29
+
30
+ let protocolVersion = 1
31
+ let maxInputBytes = 96 * 1024 * 1024
32
+ let maxStrokes = 20_000
33
+ let maxPoints = 1_000_000
34
+
35
+ // MARK: - Output
36
+
37
+ struct HelperFailure: Error {
38
+ let code: String
39
+ let message: String
40
+ }
41
+
42
+ func writeJSON(_ object: [String: Any]) {
43
+ let data: Data
44
+ if JSONSerialization.isValidJSONObject(object),
45
+ let encoded = try? JSONSerialization.data(withJSONObject: object, options: [.sortedKeys]) {
46
+ data = encoded
47
+ } else {
48
+ data = Data(#"{"code":"encode_failed","message":"response was not JSON-encodable","status":"error"}"#.utf8)
49
+ }
50
+ FileHandle.standardOutput.write(data)
51
+ FileHandle.standardOutput.write(Data([0x0A]))
52
+ }
53
+
54
+ func finish(_ object: [String: Any]) -> Never {
55
+ writeJSON(object)
56
+ exit(object["status"] as? String == "ok" ? 0 : 1)
57
+ }
58
+
59
+ func fail(_ failure: HelperFailure) -> Never {
60
+ finish(["status": "error", "code": failure.code, "message": failure.message])
61
+ }
62
+
63
+ /// Round to two decimals so large drawings stay compact on the wire.
64
+ func rounded(_ value: CGFloat) -> Double {
65
+ (Double(value) * 100).rounded() / 100
66
+ }
67
+
68
+ // MARK: - Drawings
69
+
70
+ func rgba(_ color: NSColor) -> [String: Any] {
71
+ let srgb = color.usingColorSpace(.sRGB) ?? color
72
+ func channel(_ value: CGFloat) -> Int { Int((min(max(value, 0), 1) * 255).rounded()) }
73
+ return [
74
+ "red": channel(srgb.redComponent),
75
+ "green": channel(srgb.greenComponent),
76
+ "blue": channel(srgb.blueComponent),
77
+ "alpha": rounded(srgb.alphaComponent),
78
+ ]
79
+ }
80
+
81
+ func decodeDrawing(_ request: [String: Any]) throws -> [String: Any] {
82
+ guard let encoded = request["dataBase64"] as? String,
83
+ let data = Data(base64Encoded: encoded)
84
+ else { throw HelperFailure(code: "invalid_request", message: "dataBase64 is required") }
85
+ if data.isEmpty { throw HelperFailure(code: "invalid_request", message: "drawing data is empty") }
86
+ let includePoints = request["includePoints"] as? Bool ?? true
87
+
88
+ let drawing: PKDrawing
89
+ do {
90
+ drawing = try PKDrawing(data: data)
91
+ } catch {
92
+ throw HelperFailure(
93
+ code: "undecodable",
94
+ message: "PencilKit could not read this drawing: \(error.localizedDescription)"
95
+ )
96
+ }
97
+
98
+ var strokes: [[String: Any]] = []
99
+ var pointBudget = maxPoints
100
+ var truncated = false
101
+ for stroke in drawing.strokes {
102
+ if strokes.count >= maxStrokes || pointBudget <= 0 {
103
+ truncated = true
104
+ break
105
+ }
106
+ let path = stroke.path
107
+ let transform = stroke.transform
108
+ var points: [[String: Any]] = []
109
+ var widthSum: CGFloat = 0
110
+ var count = 0
111
+ for point in path {
112
+ count += 1
113
+ widthSum += point.size.width
114
+ if includePoints {
115
+ if pointBudget <= 0 {
116
+ truncated = true
117
+ break
118
+ }
119
+ pointBudget -= 1
120
+ let location = point.location.applying(transform)
121
+ points.append([
122
+ "x": rounded(location.x),
123
+ "y": rounded(location.y),
124
+ "width": rounded(point.size.width),
125
+ "opacity": rounded(point.opacity),
126
+ "force": rounded(point.force),
127
+ ])
128
+ }
129
+ }
130
+ let bounds = stroke.renderBounds
131
+ var entry: [String: Any] = [
132
+ "inkType": stroke.ink.inkType.rawValue,
133
+ "color": rgba(stroke.ink.color),
134
+ "width": count > 0 ? rounded(widthSum / CGFloat(count)) : 0,
135
+ "pointCount": count,
136
+ "bounds": [
137
+ "x": rounded(bounds.origin.x), "y": rounded(bounds.origin.y),
138
+ "width": rounded(bounds.size.width), "height": rounded(bounds.size.height),
139
+ ],
140
+ ]
141
+ if includePoints { entry["points"] = points }
142
+ if !transform.isIdentity { entry["transformApplied"] = true }
143
+ strokes.append(entry)
144
+ }
145
+ let bounds = drawing.bounds
146
+ return [
147
+ "status": "ok",
148
+ "strokeCount": drawing.strokes.count,
149
+ "strokes": strokes,
150
+ "truncated": truncated,
151
+ "bounds": [
152
+ "x": rounded(bounds.origin.x), "y": rounded(bounds.origin.y),
153
+ "width": rounded(bounds.size.width), "height": rounded(bounds.size.height),
154
+ ],
155
+ ]
156
+ }
157
+
158
+ func inkType(named name: String) -> PKInk.InkType {
159
+ switch name {
160
+ case "com.apple.ink.marker", "marker": return .marker
161
+ case "com.apple.ink.pencil", "pencil": return .pencil
162
+ default: return .pen
163
+ }
164
+ }
165
+
166
+ func encodeDrawing(_ request: [String: Any]) throws -> [String: Any] {
167
+ guard let specs = request["strokes"] as? [[String: Any]], !specs.isEmpty else {
168
+ throw HelperFailure(code: "invalid_request", message: "strokes must be a non-empty array")
169
+ }
170
+ if specs.count > maxStrokes {
171
+ throw HelperFailure(code: "invalid_request", message: "too many strokes")
172
+ }
173
+ var strokes: [PKStroke] = []
174
+ let created = Date(timeIntervalSince1970: 0)
175
+ for spec in specs {
176
+ guard let rawPoints = spec["points"] as? [[String: Any]], rawPoints.count >= 2 else {
177
+ throw HelperFailure(code: "invalid_request", message: "each stroke needs at least two points")
178
+ }
179
+ let width = CGFloat((spec["width"] as? NSNumber)?.doubleValue ?? 3)
180
+ let color = spec["color"] as? [String: Any] ?? [:]
181
+ func component(_ key: String, _ fallback: Double) -> CGFloat {
182
+ CGFloat((color[key] as? NSNumber)?.doubleValue ?? fallback)
183
+ }
184
+ let ink = PKInk(
185
+ inkType(named: spec["inkType"] as? String ?? "pen"),
186
+ color: NSColor(
187
+ srgbRed: component("red", 0) / 255,
188
+ green: component("green", 0) / 255,
189
+ blue: component("blue", 0) / 255,
190
+ alpha: component("alpha", 1)
191
+ )
192
+ )
193
+ let last = Double(rawPoints.count - 1)
194
+ let controlPoints = rawPoints.enumerated().map { index, point in
195
+ PKStrokePoint(
196
+ location: CGPoint(
197
+ x: (point["x"] as? NSNumber)?.doubleValue ?? 0,
198
+ y: (point["y"] as? NSNumber)?.doubleValue ?? 0
199
+ ),
200
+ timeOffset: Double(index) / last,
201
+ size: CGSize(width: width, height: width),
202
+ opacity: 1,
203
+ force: 1,
204
+ azimuth: 0,
205
+ altitude: .pi / 2
206
+ )
207
+ }
208
+ strokes.append(PKStroke(ink: ink, path: PKStrokePath(controlPoints: controlPoints, creationDate: created)))
209
+ }
210
+ let data = PKDrawing(strokes: strokes).dataRepresentation()
211
+ return ["status": "ok", "strokeCount": strokes.count, "dataBase64": data.base64EncodedString()]
212
+ }
213
+
214
+ // MARK: - Dispatch
215
+
216
+ let actions = ["hello", "decode_drawing", "encode_drawing"]
217
+
218
+ func handle(_ request: [String: Any]) throws -> [String: Any] {
219
+ guard (request["protocol"] as? NSNumber)?.intValue == protocolVersion else {
220
+ throw HelperFailure(code: "protocol_mismatch", message: "expected protocol \(protocolVersion)")
221
+ }
222
+ switch request["action"] as? String {
223
+ case "hello":
224
+ return [
225
+ "status": "ok",
226
+ "protocolVersion": protocolVersion,
227
+ "sourceSha256": helperSourceSHA256,
228
+ "actions": actions,
229
+ ]
230
+ case "decode_drawing":
231
+ return try decodeDrawing(request)
232
+ case "encode_drawing":
233
+ return try encodeDrawing(request)
234
+ default:
235
+ throw HelperFailure(code: "unknown_action", message: "unknown action")
236
+ }
237
+ }
238
+
239
+ @main
240
+ struct PublicHelper {
241
+ static func main() {
242
+ let input = FileHandle.standardInput.readDataToEndOfFile()
243
+ if input.count > maxInputBytes {
244
+ fail(HelperFailure(code: "invalid_request", message: "request exceeds \(maxInputBytes) bytes"))
245
+ }
246
+ guard let request = (try? JSONSerialization.jsonObject(with: input)) as? [String: Any] else {
247
+ fail(HelperFailure(code: "invalid_request", message: "stdin must be one JSON object"))
248
+ }
249
+ do {
250
+ finish(try handle(request))
251
+ } catch let failure as HelperFailure {
252
+ fail(failure)
253
+ } catch {
254
+ fail(HelperFailure(code: "internal_error", message: error.localizedDescription))
255
+ }
256
+ }
257
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.9.5",
3
+ "version": "2.9.7",
4
4
  "description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",