apple-notes-mcp 2.15.2 → 2.15.3

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
@@ -2899,6 +2899,35 @@ title, modification date, folder identifier, lock/trash/shared/editable
2899
2899
  flags, iCloud version counters, and a `revision` change token (compare two
2900
2900
  reads to detect a change). Opens the store with Core Data's read-only option.
2901
2901
 
2902
+ #### `native-checklist-state`
2903
+
2904
+ Reads one note's native checklist by Notes UUID (`identifier`) or x-coredata
2905
+ `id`; pass exactly one. The `id` is resolved through an exact read-only database
2906
+ lookup after checking the opt-in and installed hashes. Requires the same opt-in, current helper, and Full Disk
2907
+ Access as `native-note-state`. Check `native-helper-status`'s `readChecklist`
2908
+ feature first: its probe checks the checklist getters separately. Locked notes
2909
+ or unavailable native bodies are refused. Ordinary `☐` glyphs and Markdown
2910
+ task text do not count as native checklist items.
2911
+
2912
+ Returns `identifier`, `revision`, `items`, `total`, `checked`, and
2913
+ `syncHostRunning`. Each item has its exact native `todoIdentifier` (32 lowercase
2914
+ hex digits), `uuid`, `index`, `done`, and `text`. Items follow the first occurrence
2915
+ of each native todo identity, which can cover multiple style runs. `text` is the
2916
+ line containing its first visible styled character, or the first styled line
2917
+ when it has only line terminators. `lineStart`, `lineLengthUTF16`, `styledStart`,
2918
+ and `styledLengthUTF16` are UTF-16 offsets and lengths; line text excludes the
2919
+ terminator. The styled range is an envelope, including intervening characters
2920
+ when the identity occurs in separated runs.
2921
+
2922
+ Read the ambiguity flags before treating an entry as a single checklist row:
2923
+ `contiguous: false` means separated runs share the identity; `consistent: false`
2924
+ means their done bits disagree; `spansLines: true` means its styled envelope
2925
+ contains multiple lines after leading/trailing terminators are removed. `done`
2926
+ comes from the first visible styled character, falling back to the first run.
2927
+ Totals count native identities, including ambiguous ones. The `revision` is an
2928
+ informational change token. This tool opens the store read-only and cannot
2929
+ toggle, create, or repair checklist items.
2930
+
2902
2931
  ## Usage Patterns
2903
2932
 
2904
2933
  ### Basic Workflow
package/build/index.js CHANGED
@@ -60365,7 +60365,8 @@ var TIMEOUT_ENV = "APPLE_NOTES_MCP_PRIVATE_HELPER_TIMEOUT_MS";
60365
60365
  var READ_ONLY_ACTIONS = /* @__PURE__ */ new Set([
60366
60366
  "hello",
60367
60367
  "probe",
60368
- "read_note_state"
60368
+ "read_note_state",
60369
+ "read_checklist"
60369
60370
  ]);
60370
60371
  var HELPER_BINARY_NAME = "apple-notes-private-helper";
60371
60372
  var HELPER_SOURCE_RELATIVE = "native/private-helper/apple-notes-private-helper.m";
@@ -60481,6 +60482,16 @@ var PrivateHelperError = class extends Error {
60481
60482
  code;
60482
60483
  details;
60483
60484
  };
60485
+ function assertPrivateHelperReady(deps = defaultDeps2()) {
60486
+ if (!privateHelperEnabled(deps.env))
60487
+ throw new PrivateHelperError(
60488
+ "disabled",
60489
+ `The private helper is off. Set ${ENABLE_ENV}=1 to opt in.`
60490
+ );
60491
+ const install = inspectInstallation(deps);
60492
+ if (!install.ready)
60493
+ throw new PrivateHelperError(install.reason || "helper_not_installed", install.detail || "");
60494
+ }
60484
60495
  var errorSchema2 = external_exports.object({
60485
60496
  status: external_exports.literal("error"),
60486
60497
  code: external_exports.string(),
@@ -60511,7 +60522,7 @@ var probeSchema = external_exports.object({
60511
60522
  noteRows: external_exports.number().int().nullable()
60512
60523
  }).passthrough(),
60513
60524
  syncHostRunning: external_exports.boolean(),
60514
- features: external_exports.object({ readNoteState: featureSchema }).passthrough()
60525
+ features: external_exports.object({ readNoteState: featureSchema, readChecklist: featureSchema }).passthrough()
60515
60526
  }).passthrough();
60516
60527
  var cloudSyncSchema = external_exports.object({
60517
60528
  available: external_exports.boolean(),
@@ -60535,12 +60546,53 @@ var noteStateSchema = external_exports.object({
60535
60546
  cloudSync: cloudSyncSchema,
60536
60547
  syncHostRunning: external_exports.boolean()
60537
60548
  }).passthrough();
60549
+ var checklistItemSchema = external_exports.object({
60550
+ todoIdentifier: external_exports.string().regex(/^[a-f0-9]{32}$/),
60551
+ uuid: external_exports.string().uuid(),
60552
+ index: external_exports.number().int().nonnegative(),
60553
+ done: external_exports.boolean(),
60554
+ text: external_exports.string(),
60555
+ lineStart: external_exports.number().int().nonnegative(),
60556
+ lineLengthUTF16: external_exports.number().int().nonnegative(),
60557
+ styledStart: external_exports.number().int().nonnegative(),
60558
+ styledLengthUTF16: external_exports.number().int().positive(),
60559
+ contiguous: external_exports.boolean(),
60560
+ consistent: external_exports.boolean(),
60561
+ spansLines: external_exports.boolean()
60562
+ });
60563
+ var checklistStateSchema = external_exports.object({
60564
+ status: external_exports.literal("ok"),
60565
+ identifier: external_exports.string().uuid(),
60566
+ revision: external_exports.string().regex(/^r1:[a-f0-9]{64}$/),
60567
+ items: external_exports.array(checklistItemSchema),
60568
+ total: external_exports.number().int().nonnegative(),
60569
+ checked: external_exports.number().int().nonnegative(),
60570
+ syncHostRunning: external_exports.boolean()
60571
+ }).passthrough().superRefine((state, ctx) => {
60572
+ const identities = /* @__PURE__ */ new Set();
60573
+ if (state.total !== state.items.length || state.checked !== state.items.filter((item) => item.done).length)
60574
+ ctx.addIssue({ code: "custom", message: "Checklist totals disagree with items" });
60575
+ state.items.forEach((item, index) => {
60576
+ if (item.index !== index || identities.has(item.todoIdentifier) || item.uuid.replaceAll("-", "").toLowerCase() !== item.todoIdentifier || item.text.length !== item.lineLengthUTF16)
60577
+ ctx.addIssue({
60578
+ code: "custom",
60579
+ path: ["items", index],
60580
+ message: "Invalid checklist identity or line"
60581
+ });
60582
+ identities.add(item.todoIdentifier);
60583
+ });
60584
+ });
60538
60585
  function callPrivateHelper(action, fields = {}, deps = defaultDeps2(), options = {}) {
60539
60586
  if (!READ_ONLY_ACTIONS.has(action))
60540
60587
  throw new PrivateHelperError(
60541
60588
  "unknown_action",
60542
60589
  `The private helper is read-only; "${action}" is not a supported action.`
60543
60590
  );
60591
+ if (Object.hasOwn(fields, "action") || Object.hasOwn(fields, "protocol"))
60592
+ throw new PrivateHelperError(
60593
+ "invalid_request",
60594
+ "Request fields cannot override action or protocol"
60595
+ );
60544
60596
  if (!options.allowDisabled && !privateHelperEnabled(deps.env))
60545
60597
  throw new PrivateHelperError(
60546
60598
  "disabled",
@@ -60627,6 +60679,19 @@ function readNoteState(identifier, deps = defaultDeps2()) {
60627
60679
  assertNoteIdentifier(identifier);
60628
60680
  return parseOrThrow(noteStateSchema, callPrivateHelper("read_note_state", { identifier }, deps));
60629
60681
  }
60682
+ function readChecklistState(identifier, deps = defaultDeps2()) {
60683
+ assertNoteIdentifier(identifier);
60684
+ const state = parseOrThrow(
60685
+ checklistStateSchema,
60686
+ callPrivateHelper("read_checklist", { identifier }, deps)
60687
+ );
60688
+ if (state.identifier.toLowerCase() !== identifier.toLowerCase())
60689
+ throw new PrivateHelperError(
60690
+ "invalid_response",
60691
+ "The helper returned a different note identifier"
60692
+ );
60693
+ return state;
60694
+ }
60630
60695
  function featureFromProbe(feature) {
60631
60696
  if (!feature) return { available: false, reason: "private_api_unavailable", detail: null };
60632
60697
  if (feature.available) return { available: true, reason: null, detail: null };
@@ -60645,7 +60710,7 @@ function privateHelperCapabilities(deps = defaultDeps2()) {
60645
60710
  reason,
60646
60711
  detail
60647
60712
  });
60648
- const both = (status) => ({ readNoteState: status });
60713
+ const both = (status) => ({ readNoteState: status, readChecklist: status });
60649
60714
  if (installation.reason === "unsupported_platform")
60650
60715
  return {
60651
60716
  enabled,
@@ -60688,7 +60753,10 @@ function privateHelperCapabilities(deps = defaultDeps2()) {
60688
60753
  readOnly: true,
60689
60754
  installation,
60690
60755
  probe,
60691
- features: { readNoteState: featureFromProbe(probe.features.readNoteState) }
60756
+ features: {
60757
+ readNoteState: featureFromProbe(probe.features.readNoteState),
60758
+ readChecklist: featureFromProbe(probe.features.readChecklist)
60759
+ }
60692
60760
  };
60693
60761
  }
60694
60762
 
@@ -61870,19 +61938,19 @@ function relayBroker(socket, leftover, deps) {
61870
61938
  // src/tools/privateHelperTools.ts
61871
61939
  var coreDataId2 = external_exports.string().regex(/^x-coredata:\/\/[0-9A-F-]+\/ICNote\/p\d+$/i);
61872
61940
  var notesUuid = external_exports.string().regex(UUID_PATTERN);
61873
- function resolveIdentifier(manager, args) {
61941
+ function resolveIdentifier(deps, args) {
61874
61942
  if (args.identifier && args.id)
61875
61943
  throw new PrivateHelperError("invalid_request", "Pass identifier or id, not both");
61876
61944
  if (args.identifier) return args.identifier;
61877
61945
  if (!args.id) throw new PrivateHelperError("invalid_request", "identifier or id is required");
61878
- const link = manager.getNoteLinkById(args.id);
61879
- const match = link?.match(/identifier=([0-9A-F-]{36})$/i);
61880
- if (!match)
61946
+ assertPrivateHelperReady(deps);
61947
+ const identifier = lookupStableIdentifiers([args.id], "ICNote").get(args.id)?.identifier;
61948
+ if (!identifier || !UUID_PATTERN.test(identifier))
61881
61949
  throw new PrivateHelperError(
61882
61950
  "not_found",
61883
61951
  "Could not resolve that id to a Notes UUID (needs Full Disk Access); pass identifier instead"
61884
61952
  );
61885
- return match[1];
61953
+ return identifier;
61886
61954
  }
61887
61955
  function envelopeCode(helperCode, message) {
61888
61956
  switch (helperCode) {
@@ -61928,7 +61996,7 @@ function helperErrorResult(error2) {
61928
61996
  })
61929
61997
  );
61930
61998
  }
61931
- function registerPrivateHelperTools(server2, manager, depsFactory = () => defaultDeps2()) {
61999
+ function registerPrivateHelperTools(server2, _manager, depsFactory = () => defaultDeps2()) {
61932
62000
  function tool(name, description, inputSchema, annotations, handler) {
61933
62001
  server2.registerTool(
61934
62002
  name,
@@ -61953,7 +62021,7 @@ function registerPrivateHelperTools(server2, manager, depsFactory = () => defaul
61953
62021
  }
61954
62022
  tool(
61955
62023
  "native-helper-status",
61956
- "Use when: checking whether the opt-in, read-only native private helper is enabled, built, current, and working on this macOS before calling native-note-state.\nReturns: enabled flag, installation state (path, manifest, stale/modified checks), the live probe (macOS and Notes versions, framework, store access), and per-feature availability with a machine reason.\nDo not use when: checking the Shortcuts bridges (native-tags-status, get-capabilities).\nSafety: read-only. The helper is read-only by design (write support was deliberately deferred by the maintainer); the probe opens the Notes store read-only and only when the helper is enabled and installed.",
62024
+ "Use when: checking whether the opt-in, read-only native private helper is enabled, built, current, and working on this macOS before calling native-note-state or native-checklist-state.\nReturns: enabled flag, installation state (path, manifest, stale/modified checks), the live probe (macOS and Notes versions, framework, store access), and per-feature availability with a machine reason.\nDo not use when: checking the Shortcuts bridges (native-tags-status, get-capabilities).\nSafety: read-only. The helper is read-only by design (write support was deliberately deferred by the maintainer); the probe opens the Notes store read-only and only when the helper is enabled and installed.",
61957
62025
  {},
61958
62026
  { readOnlyHint: true, openWorldHint: false },
61959
62027
  (_args, deps) => {
@@ -61972,7 +62040,17 @@ function registerPrivateHelperTools(server2, manager, depsFactory = () => defaul
61972
62040
  id: coreDataId2.optional().describe("x-coredata note id; resolved to a UUID via the database")
61973
62041
  },
61974
62042
  { readOnlyHint: true, openWorldHint: false },
61975
- (args, deps) => ({ ...readNoteState(resolveIdentifier(manager, args), deps) })
62043
+ (args, deps) => ({ ...readNoteState(resolveIdentifier(deps, args), deps) })
62044
+ );
62045
+ tool(
62046
+ "native-checklist-state",
62047
+ "Use when: reading a note's exact native checklist todo identifiers and done state, including ambiguous style runs.\nReturns: identifier, revision change token, items in first native-style occurrence order, total, checked, and syncHostRunning. Each item has todoIdentifier (32 lowercase hex digits), uuid, index, text and done; line and styled offsets use UTF-16. contiguous=false means the styled envelope includes gaps, consistent=false means runs disagree on done, and spansLines=true means the identity spans multiple lines.\nDo not use when: reading ordinary checkbox-like text (get-note-content) or changing checklist items. Flags describe native identity ambiguity; text is the first visible styled character's line.\nSafety: read-only; requires APPLE_NOTES_MCP_ENABLE_PRIVATE=1, Full Disk Access, and a current built helper. Locked or unavailable native bodies are refused. The helper has no write action.",
62048
+ {
62049
+ identifier: notesUuid.optional().describe("Notes UUID (the notes://showNote identifier)"),
62050
+ id: coreDataId2.optional().describe("x-coredata note id; resolved to a UUID via the database")
62051
+ },
62052
+ { readOnlyHint: true, openWorldHint: false },
62053
+ (args, deps) => ({ ...readChecklistState(resolveIdentifier(deps, args), deps) })
61976
62054
  );
61977
62055
  }
61978
62056
 
@@ -23,7 +23,7 @@ These groups use protected Notes data:
23
23
  | Portable exports | `export-notes-markdown`, `export-notes-html`, and attachment/media exports that read the Notes store |
24
24
  | Attachments and drawings | `list-attachments` with `includePaths` or `firstImage`, `export-attachments`, `list-paper-attachments`, `export-paper-image`, `get-note-drawings`, and `transcribe-note-audio` |
25
25
  | Guarded operations and identifiers | `delete-folder-by-id`; database-backed copy/active-note deletion guards; UUID or numeric note/folder selectors; native preservation/readback and Smart Folder destination detection |
26
- | Optional private helper | `native-note-state`; the helper is read-only and requires its separate opt-in and build |
26
+ | Optional private helper | `native-note-state`, `native-checklist-state`; the helper is read-only and requires its separate opt-in and build |
27
27
 
28
28
  `get-note-link` needs FDA on macOS 26 and later. On macOS 12–15 it can fall back to Notes' AppleScript link property. `get-note-markdown` can still return Markdown without FDA, but native checklist done/undone annotations are unavailable.
29
29
 
@@ -134,6 +134,14 @@ static const APIRequirement kReadAPI[] = {
134
134
  {"ICTTMergeableString", "attributedString", NO},
135
135
  };
136
136
 
137
+ // Checklist inspection needs only getters; no editing or todo constructors.
138
+ static const APIRequirement kChecklistReadAPI[] = {
139
+ {"ICTTParagraphStyle", "style", NO},
140
+ {"ICTTParagraphStyle", "todo", NO},
141
+ {"ICTTTodo", "uuid", NO},
142
+ {"ICTTTodo", "done", NO},
143
+ };
144
+
137
145
  // Core Data properties are @dynamic: their accessors do not exist until Core
138
146
  // Data generates them, so they are checked against the managed object model
139
147
  // (entity name, then property names) rather than with respondsToSelector:.
@@ -207,7 +215,7 @@ static NSArray<NSString *> *MissingModelProperties(void) {
207
215
  return missing;
208
216
  }
209
217
 
210
- typedef NS_ENUM(NSInteger, Feature) { FeatureModel, FeatureRead };
218
+ typedef NS_ENUM(NSInteger, Feature) { FeatureModel, FeatureRead, FeatureChecklistRead };
211
219
 
212
220
  static NSArray<NSString *> *MissingForFeature(Feature feature) {
213
221
  LoadFramework();
@@ -219,6 +227,8 @@ static NSArray<NSString *> *MissingForFeature(Feature feature) {
219
227
  [missing addObjectsFromArray:MissingModelProperties()];
220
228
  [missing addObjectsFromArray:MissingAPI(kReadAPI, COUNT(kReadAPI))];
221
229
  }
230
+ if (feature == FeatureChecklistRead)
231
+ [missing addObjectsFromArray:MissingAPI(kChecklistReadAPI, COUNT(kChecklistReadAPI))];
222
232
  return missing;
223
233
  }
224
234
 
@@ -401,6 +411,97 @@ static NSString *BodyText(id mergeableString) {
401
411
  return [attributed isKindOfClass:[NSAttributedString class]] ? [attributed string] : nil;
402
412
  }
403
413
 
414
+ // TTStyle runs can include the previous line's terminator. Group by native
415
+ // todo identity, not by a visible line or a checkbox-like text prefix.
416
+ // This operates only on an attributed-string snapshot, never the store.
417
+ static NSArray<NSDictionary *> *ChecklistItems(NSAttributedString *body) {
418
+ NSString *text = body.string;
419
+ NSCharacterSet *newlines = NSCharacterSet.newlineCharacterSet;
420
+ NSMutableArray<NSString *> *order = [NSMutableArray array];
421
+ NSMutableDictionary<NSString *, NSMutableDictionary *> *groups = [NSMutableDictionary dictionary];
422
+ [body enumerateAttribute:@"TTStyle"
423
+ inRange:NSMakeRange(0, body.length)
424
+ options:0
425
+ usingBlock:^(id style, NSRange run, BOOL *stop) {
426
+ (void)stop;
427
+ if (!style) return;
428
+ if (![style respondsToSelector:sel_registerName("style")])
429
+ Fail(@"private_api_unavailable", @"A native paragraph style has no style getter", nil);
430
+ unsigned int kind =
431
+ ((unsigned int (*)(id, SEL))objc_msgSend)(style, sel_registerName("style"));
432
+ if (kind != 103) return; // Notes' native checklist paragraph style.
433
+ if (![style respondsToSelector:sel_registerName("todo")])
434
+ Fail(@"private_api_unavailable", @"A native checklist style has no todo getter", nil);
435
+ id todo = Send(style, "todo");
436
+ if (!todo || ![todo respondsToSelector:sel_registerName("uuid")] ||
437
+ ![todo respondsToSelector:sel_registerName("done")])
438
+ Fail(@"private_api_unavailable", @"A native checklist todo cannot be read", nil);
439
+ id uuid = Send(todo, "uuid");
440
+ if (![uuid isKindOfClass:[NSUUID class]])
441
+ Fail(@"unsupported_note", @"A native checklist todo has no valid UUID", nil);
442
+ NSString *hex = [[[uuid UUIDString] stringByReplacingOccurrencesOfString:@"-"
443
+ withString:@""] lowercaseString];
444
+ NSMutableDictionary *group = groups[hex];
445
+ if (!group) {
446
+ group = [@{@"uuid" : [uuid UUIDString],
447
+ @"ranges" : [NSMutableArray array],
448
+ @"states" : [NSMutableSet set]} mutableCopy];
449
+ groups[hex] = group;
450
+ [order addObject:hex];
451
+ }
452
+ NSNumber *done = @(SendBool(todo, "done"));
453
+ [group[@"ranges"] addObject:[NSValue valueWithRange:run]];
454
+ [group[@"states"] addObject:done];
455
+ if (!group[@"fallbackDone"]) group[@"fallbackDone"] = done;
456
+ // Prefer the first styled visible character over a leading
457
+ // newline, whose done bit can differ in malformed content.
458
+ for (NSUInteger i = run.location; i < NSMaxRange(run); i++) {
459
+ if (![newlines characterIsMember:[text characterAtIndex:i]]) {
460
+ if (!group[@"anchor"]) {
461
+ group[@"anchor"] = @(i);
462
+ group[@"done"] = done;
463
+ }
464
+ break;
465
+ }
466
+ }
467
+ }];
468
+ NSMutableArray *items = [NSMutableArray array];
469
+ for (NSString *hex in order) {
470
+ NSDictionary *group = groups[hex];
471
+ NSArray<NSValue *> *ranges = group[@"ranges"];
472
+ NSRange first = ranges.firstObject.rangeValue, last = ranges.lastObject.rangeValue;
473
+ NSUInteger start = first.location, end = NSMaxRange(last), covered = 0;
474
+ for (NSValue *range in ranges) covered += range.rangeValue.length;
475
+ NSUInteger anchor = group[@"anchor"] ? [group[@"anchor"] unsignedIntegerValue] : start;
476
+ NSUInteger lineStart = 0, lineEnd = 0;
477
+ [text getLineStart:&lineStart end:NULL contentsEnd:&lineEnd forRange:NSMakeRange(anchor, 0)];
478
+ NSUInteger visibleStart = start, visibleEnd = end;
479
+ while (visibleStart < visibleEnd && [newlines characterIsMember:[text characterAtIndex:visibleStart]])
480
+ visibleStart++;
481
+ while (visibleEnd > visibleStart && [newlines characterIsMember:[text characterAtIndex:visibleEnd - 1]])
482
+ visibleEnd--;
483
+ BOOL spansLines = [text rangeOfCharacterFromSet:newlines
484
+ options:0
485
+ range:NSMakeRange(visibleStart, visibleEnd - visibleStart)].location != NSNotFound;
486
+ [items addObject:@{
487
+ @"todoIdentifier" : hex,
488
+ @"uuid" : group[@"uuid"],
489
+ @"index" : @(items.count),
490
+ @"done" : group[@"done"] ?: group[@"fallbackDone"],
491
+ @"text" : [text substringWithRange:NSMakeRange(lineStart, lineEnd - lineStart)],
492
+ @"lineStart" : @(lineStart),
493
+ @"lineLengthUTF16" : @(lineEnd - lineStart),
494
+ // The envelope includes gaps when contiguous is false.
495
+ @"styledStart" : @(start),
496
+ @"styledLengthUTF16" : @(end - start),
497
+ @"contiguous" : @((BOOL)(covered == end - start)),
498
+ @"consistent" : @((BOOL)([group[@"states"] count] == 1)),
499
+ @"spansLines" : @(spansLines),
500
+ }];
501
+ }
502
+ return items;
503
+ }
504
+
404
505
  static BOOL NotesAppRunning(void) {
405
506
  return [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.Notes"].count > 0;
406
507
  }
@@ -472,6 +573,7 @@ static NSString *RequireIdentifier(NSDictionary *request) {
472
573
  static NSDictionary *HandleHello(NSDictionary *request);
473
574
  static NSDictionary *HandleProbe(NSDictionary *request);
474
575
  static NSDictionary *HandleReadNoteState(NSDictionary *request);
576
+ static NSDictionary *HandleReadChecklist(NSDictionary *request);
475
577
 
476
578
  typedef struct {
477
579
  const char *name;
@@ -485,6 +587,7 @@ static const ActionSpec kActions[] = {
485
587
  {"hello", "", HandleHello},
486
588
  {"probe", "", HandleProbe},
487
589
  {"read_note_state", "identifier", HandleReadNoteState},
590
+ {"read_checklist", "identifier", HandleReadChecklist},
488
591
  };
489
592
 
490
593
  static NSArray<NSString *> *ActionNames(void) {
@@ -562,6 +665,7 @@ static NSDictionary *HandleProbe(NSDictionary *request) {
562
665
  @"syncHostRunning" : @(NotesAppRunning()),
563
666
  @"features" : @{
564
667
  @"readNoteState" : FeatureReport(FeatureRead, contextOK, contextReason),
668
+ @"readChecklist" : FeatureReport(FeatureChecklistRead, contextOK, contextReason),
565
669
  },
566
670
  };
567
671
  }
@@ -577,6 +681,33 @@ static NSDictionary *HandleReadNoteState(NSDictionary *request) {
577
681
  return result;
578
682
  }
579
683
 
684
+ static NSDictionary *HandleReadChecklist(NSDictionary *request) {
685
+ NSString *identifier = RequireIdentifier(request);
686
+ RequireFeature(FeatureChecklistRead);
687
+ NSManagedObjectContext *context = OpenReadOnlyContext(ResolveStore());
688
+ NSManagedObject *note = FetchNote(context, identifier);
689
+ if (SendBool(note, "isPasswordProtected"))
690
+ Fail(@"unsupported_note", @"Locked notes are not supported", nil);
691
+ id mergeable = Send(note, "mergeableString");
692
+ if (!mergeable || ![mergeable respondsToSelector:sel_registerName("attributedString")])
693
+ Fail(@"unsupported_note", @"The note's native body is unavailable", nil);
694
+ id body = Send(mergeable, "attributedString");
695
+ if (![body isKindOfClass:[NSAttributedString class]])
696
+ Fail(@"unsupported_note", @"The note's native body is unavailable", nil);
697
+ NSArray<NSDictionary *> *items = ChecklistItems(body);
698
+ NSUInteger checked = 0;
699
+ for (NSDictionary *item in items) if ([item[@"done"] boolValue]) checked++;
700
+ return @{
701
+ @"status" : @"ok",
702
+ @"identifier" : identifier,
703
+ @"revision" : RevisionToken(note),
704
+ @"items" : items,
705
+ @"total" : @(items.count),
706
+ @"checked" : @(checked),
707
+ @"syncHostRunning" : @(NotesAppRunning()),
708
+ };
709
+ }
710
+
580
711
  #pragma mark - Main
581
712
 
582
713
  static NSData *ReadStdin(void) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.15.2",
3
+ "version": "2.15.3",
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",