apple-notes-mcp 1.1.2 → 1.2.1

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/build/index.js CHANGED
@@ -31,7 +31,7 @@ import { AppleNotesManager } from "./services/appleNotesManager.js";
31
31
  */
32
32
  const server = new McpServer({
33
33
  name: "apple-notes",
34
- version: "1.1.0",
34
+ version: "1.2.0",
35
35
  description: "MCP server for managing Apple Notes - create, search, update, and organize notes",
36
36
  });
37
37
  /**
@@ -113,7 +113,7 @@ server.tool("create-note", {
113
113
  if (!note) {
114
114
  return errorResponse(`Failed to create note "${title}". Check that Notes.app is configured and accessible.`);
115
115
  }
116
- return successResponse(`Note created: "${note.title}"`);
116
+ return successResponse(`Note created: "${note.title}" [id: ${note.id}]`);
117
117
  }, "Error creating note"));
118
118
  // --- search-notes ---
119
119
  server.tool("search-notes", {
@@ -126,22 +126,42 @@ server.tool("search-notes", {
126
126
  if (notes.length === 0) {
127
127
  return successResponse(`No notes found matching "${query}" in ${searchType}`);
128
128
  }
129
- // Format each note with folder info, highlighting Recently Deleted
129
+ // Format each note with ID and folder info, highlighting Recently Deleted
130
130
  const noteList = notes
131
131
  .map((n) => {
132
+ const idSuffix = n.id ? ` [id: ${n.id}]` : "";
132
133
  if (n.folder === "Recently Deleted") {
133
- return ` - ${n.title} [DELETED]`;
134
+ return ` - ${n.title} [DELETED]${idSuffix}`;
134
135
  }
135
136
  else if (n.folder) {
136
- return ` - ${n.title} (${n.folder})`;
137
+ return ` - ${n.title} (${n.folder})${idSuffix}`;
137
138
  }
138
- return ` - ${n.title}`;
139
+ return ` - ${n.title}${idSuffix}`;
139
140
  })
140
141
  .join("\n");
141
142
  return successResponse(`Found ${notes.length} notes (searched ${searchType}):\n${noteList}`);
142
143
  }, "Error searching notes"));
143
144
  // --- get-note-content ---
144
- server.tool("get-note-content", noteTitleSchema, withErrorHandling(({ title, account }) => {
145
+ server.tool("get-note-content", {
146
+ id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
147
+ title: z.string().optional().describe("Note title (use id instead when available)"),
148
+ account: z
149
+ .string()
150
+ .optional()
151
+ .describe("Account name (defaults to iCloud, ignored if id is provided)"),
152
+ }, withErrorHandling(({ id, title, account }) => {
153
+ // Prefer ID-based lookup if provided
154
+ if (id) {
155
+ const content = notesManager.getNoteContentById(id);
156
+ if (!content) {
157
+ return errorResponse(`Note with ID "${id}" not found`);
158
+ }
159
+ return successResponse(content);
160
+ }
161
+ // Fall back to title-based lookup
162
+ if (!title) {
163
+ return errorResponse("Either 'id' or 'title' is required");
164
+ }
145
165
  const content = notesManager.getNoteContent(title, account);
146
166
  if (!content) {
147
167
  return errorResponse(`Note "${title}" not found`);
@@ -187,11 +207,28 @@ server.tool("get-note-details", noteTitleSchema, withErrorHandling(({ title, acc
187
207
  }, "Error retrieving note details"));
188
208
  // --- update-note ---
189
209
  server.tool("update-note", {
190
- title: z.string().min(1, "Current note title is required"),
210
+ id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
211
+ title: z.string().optional().describe("Current note title (use id instead when available)"),
191
212
  newTitle: z.string().optional().describe("New title for the note"),
192
213
  newContent: z.string().min(1, "New content is required"),
193
- account: z.string().optional().describe("Account containing the note"),
194
- }, withErrorHandling(({ title, newTitle, newContent, account }) => {
214
+ account: z
215
+ .string()
216
+ .optional()
217
+ .describe("Account containing the note (ignored if id is provided)"),
218
+ }, withErrorHandling(({ id, title, newTitle, newContent, account }) => {
219
+ // Prefer ID-based update if provided
220
+ if (id) {
221
+ const success = notesManager.updateNoteById(id, newTitle, newContent);
222
+ if (!success) {
223
+ return errorResponse(`Failed to update note with ID "${id}". Note may not exist.`);
224
+ }
225
+ const displayTitle = newTitle || "(title preserved)";
226
+ return successResponse(`Note updated: "${displayTitle}"`);
227
+ }
228
+ // Fall back to title-based update
229
+ if (!title) {
230
+ return errorResponse("Either 'id' or 'title' is required");
231
+ }
195
232
  const success = notesManager.updateNote(title, newTitle, newContent, account);
196
233
  if (!success) {
197
234
  return errorResponse(`Failed to update note "${title}". Note may not exist.`);
@@ -200,7 +237,26 @@ server.tool("update-note", {
200
237
  return successResponse(`Note updated: "${finalTitle}"`);
201
238
  }, "Error updating note"));
202
239
  // --- delete-note ---
203
- server.tool("delete-note", noteTitleSchema, withErrorHandling(({ title, account }) => {
240
+ server.tool("delete-note", {
241
+ id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
242
+ title: z.string().optional().describe("Note title (use id instead when available)"),
243
+ account: z
244
+ .string()
245
+ .optional()
246
+ .describe("Account name (defaults to iCloud, ignored if id is provided)"),
247
+ }, withErrorHandling(({ id, title, account }) => {
248
+ // Prefer ID-based deletion if provided
249
+ if (id) {
250
+ const success = notesManager.deleteNoteById(id);
251
+ if (!success) {
252
+ return errorResponse(`Failed to delete note with ID "${id}". Note may not exist.`);
253
+ }
254
+ return successResponse(`Note deleted (by ID)`);
255
+ }
256
+ // Fall back to title-based deletion
257
+ if (!title) {
258
+ return errorResponse("Either 'id' or 'title' is required");
259
+ }
204
260
  const success = notesManager.deleteNote(title, account);
205
261
  if (!success) {
206
262
  return errorResponse(`Failed to delete note "${title}". Note may not exist.`);
@@ -209,10 +265,23 @@ server.tool("delete-note", noteTitleSchema, withErrorHandling(({ title, account
209
265
  }, "Error deleting note"));
210
266
  // --- move-note ---
211
267
  server.tool("move-note", {
212
- title: z.string().min(1, "Note title is required"),
268
+ id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
269
+ title: z.string().optional().describe("Note title (use id instead when available)"),
213
270
  folder: z.string().min(1, "Destination folder is required"),
214
- account: z.string().optional().describe("Account containing the note"),
215
- }, withErrorHandling(({ title, folder, account }) => {
271
+ account: z.string().optional().describe("Account containing the note/folder"),
272
+ }, withErrorHandling(({ id, title, folder, account }) => {
273
+ // Prefer ID-based move if provided
274
+ if (id) {
275
+ const success = notesManager.moveNoteById(id, folder, account);
276
+ if (!success) {
277
+ return errorResponse(`Failed to move note with ID "${id}" to folder "${folder}". Note or folder may not exist.`);
278
+ }
279
+ return successResponse(`Note moved to "${folder}" (by ID)`);
280
+ }
281
+ // Fall back to title-based move
282
+ if (!title) {
283
+ return errorResponse("Either 'id' or 'title' is required");
284
+ }
216
285
  const success = notesManager.moveNote(title, folder, account);
217
286
  if (!success) {
218
287
  return errorResponse(`Failed to move note "${title}" to folder "${folder}". Note or folder may not exist.`);
@@ -262,15 +262,22 @@ export class AppleNotesManager {
262
262
  const safeContent = escapeForAppleScript(content);
263
263
  // Build the AppleScript command
264
264
  // Notes.app uses 'name' for the title and 'body' for content
265
+ // We capture the ID of the newly created note
265
266
  let createCommand;
266
267
  if (folder) {
267
268
  // Create note in specific folder
268
269
  const safeFolder = escapeForAppleScript(folder);
269
- createCommand = `make new note at folder "${safeFolder}" with properties {name:"${safeTitle}", body:"${safeContent}"}`;
270
+ createCommand = `
271
+ set newNote to make new note at folder "${safeFolder}" with properties {name:"${safeTitle}", body:"${safeContent}"}
272
+ return id of newNote
273
+ `;
270
274
  }
271
275
  else {
272
276
  // Create note in default location
273
- createCommand = `make new note with properties {name:"${safeTitle}", body:"${safeContent}"}`;
277
+ createCommand = `
278
+ set newNote to make new note with properties {name:"${safeTitle}", body:"${safeContent}"}
279
+ return id of newNote
280
+ `;
274
281
  }
275
282
  // Execute the script
276
283
  const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
@@ -279,11 +286,12 @@ export class AppleNotesManager {
279
286
  console.error(`Failed to create note "${title}":`, result.error);
280
287
  return null;
281
288
  }
282
- // Return a Note object representing the created note
283
- // Note: We use a timestamp as ID since we can't easily get the real ID
289
+ // Extract the CoreData ID from the response
290
+ const noteId = result.output.trim();
291
+ // Return a Note object representing the created note with real ID
284
292
  const now = new Date();
285
293
  return {
286
- id: Date.now().toString(),
294
+ id: noteId || Date.now().toString(), // Use real ID, fallback to timestamp
287
295
  title,
288
296
  content,
289
297
  tags,
@@ -321,8 +329,8 @@ export class AppleNotesManager {
321
329
  const whereClause = searchContent
322
330
  ? `body contains "${safeQuery}"`
323
331
  : `name contains "${safeQuery}"`;
324
- // Get names and folder for each matching note
325
- // We use a repeat loop to get both properties, separated by a delimiter
332
+ // Get names, IDs, and folder for each matching note
333
+ // We use a repeat loop to get all properties, separated by a delimiter
326
334
  // Note: Some notes may have inaccessible containers, so we wrap in try/on error
327
335
  const searchCommand = `
328
336
  set matchingNotes to notes where ${whereClause}
@@ -330,12 +338,14 @@ export class AppleNotesManager {
330
338
  repeat with n in matchingNotes
331
339
  try
332
340
  set noteName to name of n
341
+ set noteId to id of n
333
342
  set noteFolder to name of container of n
334
- set end of resultList to noteName & "|||" & noteFolder
343
+ set end of resultList to noteName & "|||" & noteId & "|||" & noteFolder
335
344
  on error
336
345
  try
337
346
  set noteName to name of n
338
- set end of resultList to noteName & "|||" & "Notes"
347
+ set noteId to id of n
348
+ set end of resultList to noteName & "|||" & noteId & "|||" & "Notes"
339
349
  end try
340
350
  end try
341
351
  end repeat
@@ -352,15 +362,15 @@ export class AppleNotesManager {
352
362
  if (!result.output.trim()) {
353
363
  return [];
354
364
  }
355
- // Parse the delimited output: "name|||folder|||ITEM|||name|||folder..."
365
+ // Parse the delimited output: "name|||id|||folder|||ITEM|||name|||id|||folder..."
356
366
  const items = result.output.split("|||ITEM|||");
357
367
  const notes = [];
358
368
  for (const item of items) {
359
- const [title, folder] = item.split("|||");
369
+ const [title, id, folder] = item.split("|||");
360
370
  if (!title?.trim())
361
371
  continue;
362
372
  notes.push({
363
- id: Date.now().toString(),
373
+ id: id?.trim() || Date.now().toString(), // Use real ID, fallback to timestamp
364
374
  title: title.trim(),
365
375
  content: "", // Not fetched in search
366
376
  tags: [],
@@ -400,6 +410,26 @@ export class AppleNotesManager {
400
410
  }
401
411
  return result.output;
402
412
  }
413
+ /**
414
+ * Retrieves the HTML content of a note by its CoreData ID.
415
+ *
416
+ * This is more reliable than getNoteContent() because IDs are unique
417
+ * across all accounts, while titles can be duplicated.
418
+ *
419
+ * @param id - CoreData URL identifier for the note
420
+ * @returns HTML content of the note, or empty string if not found
421
+ */
422
+ getNoteContentById(id) {
423
+ // Note IDs work at the application level, not scoped to account
424
+ const getCommand = `get body of note id "${id}"`;
425
+ const script = buildAppLevelScript(getCommand);
426
+ const result = executeAppleScript(script);
427
+ if (!result.success) {
428
+ console.error(`Failed to get content of note with ID "${id}":`, result.error);
429
+ return "";
430
+ }
431
+ return result.output;
432
+ }
403
433
  /**
404
434
  * Retrieves a note by its unique CoreData ID.
405
435
  *
@@ -533,6 +563,25 @@ export class AppleNotesManager {
533
563
  }
534
564
  return true;
535
565
  }
566
+ /**
567
+ * Deletes a note by its CoreData ID.
568
+ *
569
+ * This is more reliable than deleteNote() because IDs are unique
570
+ * across all accounts, while titles can be duplicated.
571
+ *
572
+ * @param id - CoreData URL identifier for the note
573
+ * @returns true if deletion succeeded, false otherwise
574
+ */
575
+ deleteNoteById(id) {
576
+ const deleteCommand = `delete note id "${id}"`;
577
+ const script = buildAppLevelScript(deleteCommand);
578
+ const result = executeAppleScript(script);
579
+ if (!result.success) {
580
+ console.error(`Failed to delete note with ID "${id}":`, result.error);
581
+ return false;
582
+ }
583
+ return true;
584
+ }
536
585
  /**
537
586
  * Updates an existing note's content and optionally its title.
538
587
  *
@@ -564,6 +613,41 @@ export class AppleNotesManager {
564
613
  }
565
614
  return true;
566
615
  }
616
+ /**
617
+ * Updates an existing note by its CoreData ID.
618
+ *
619
+ * This is more reliable than updateNote() because IDs are unique,
620
+ * while titles can be duplicated.
621
+ *
622
+ * @param id - CoreData URL identifier for the note
623
+ * @param newTitle - New title (optional, keeps existing if not provided)
624
+ * @param newContent - New content for the note body
625
+ * @returns true if update succeeded, false otherwise
626
+ */
627
+ updateNoteById(id, newTitle, newContent) {
628
+ // First get the current title if newTitle is not provided
629
+ let effectiveTitle = newTitle;
630
+ if (!effectiveTitle) {
631
+ const note = this.getNoteById(id);
632
+ if (!note) {
633
+ console.error(`Cannot update note: note with ID "${id}" not found`);
634
+ return false;
635
+ }
636
+ effectiveTitle = note.title;
637
+ }
638
+ const safeEffectiveTitle = escapeForAppleScript(effectiveTitle);
639
+ const safeContent = escapeForAppleScript(newContent);
640
+ // Apple Notes uses HTML body; first <div> becomes the title
641
+ const fullBody = `<div>${safeEffectiveTitle}</div><div>${safeContent}</div>`;
642
+ const updateCommand = `set body of note id "${id}" to "${fullBody}"`;
643
+ const script = buildAppLevelScript(updateCommand);
644
+ const result = executeAppleScript(script);
645
+ if (!result.success) {
646
+ console.error(`Failed to update note with ID "${id}":`, result.error);
647
+ return false;
648
+ }
649
+ return true;
650
+ }
567
651
  /**
568
652
  * Lists all notes in an account, optionally filtered by folder.
569
653
  *
@@ -682,13 +766,19 @@ export class AppleNotesManager {
682
766
  */
683
767
  moveNote(title, destinationFolder, account) {
684
768
  const targetAccount = this.resolveAccount(account);
685
- // Step 1: Retrieve the original note's content
769
+ // Step 1: Get the original note's ID first (before creating a copy with the same title)
770
+ const originalNote = this.getNoteDetails(title, targetAccount);
771
+ if (!originalNote) {
772
+ console.error(`Cannot move note "${title}": note not found`);
773
+ return false;
774
+ }
775
+ // Step 2: Retrieve the original note's content
686
776
  const originalContent = this.getNoteContent(title, targetAccount);
687
777
  if (!originalContent) {
688
778
  console.error(`Cannot move note "${title}": failed to retrieve content`);
689
779
  return false;
690
780
  }
691
- // Step 2: Create a copy in the destination folder
781
+ // Step 3: Create a copy in the destination folder
692
782
  // We need to escape the HTML content for AppleScript embedding
693
783
  const safeFolder = escapeForAppleScript(destinationFolder);
694
784
  const safeContent = originalContent.replace(/"/g, '\\"').replace(/'/g, "'\\''");
@@ -699,16 +789,57 @@ export class AppleNotesManager {
699
789
  console.error(`Cannot move note "${title}": failed to create in destination folder:`, copyResult.error);
700
790
  return false;
701
791
  }
702
- // Step 3: Delete the original (only after successful copy)
703
- const deleteSuccess = this.deleteNote(title, targetAccount);
704
- if (!deleteSuccess) {
792
+ // Step 4: Delete the original by ID (not by title, since there are now two notes with the same title)
793
+ const deleteCommand = `delete note id "${originalNote.id}"`;
794
+ const deleteScript = buildAppLevelScript(deleteCommand);
795
+ const deleteResult = executeAppleScript(deleteScript);
796
+ if (!deleteResult.success) {
705
797
  // The note was copied successfully but we couldn't delete the original.
706
798
  // This is still a partial success - the note exists in the new location.
707
- console.error(`Note "${title}" was copied to "${destinationFolder}" but original could not be deleted`);
799
+ console.error(`Note "${title}" was copied to "${destinationFolder}" but original could not be deleted:`, deleteResult.error);
708
800
  return true;
709
801
  }
710
802
  return true;
711
803
  }
804
+ /**
805
+ * Moves a note to a different folder by its CoreData ID.
806
+ *
807
+ * This is more reliable than moveNote() because IDs are unique,
808
+ * while titles can be duplicated.
809
+ *
810
+ * @param id - CoreData URL identifier for the note
811
+ * @param destinationFolder - Name of the folder to move to
812
+ * @param account - Account containing the destination folder (defaults to iCloud)
813
+ * @returns true if move succeeded (or copy succeeded but delete failed)
814
+ */
815
+ moveNoteById(id, destinationFolder, account) {
816
+ const targetAccount = this.resolveAccount(account);
817
+ // Step 1: Retrieve the original note's content by ID
818
+ const originalContent = this.getNoteContentById(id);
819
+ if (!originalContent) {
820
+ console.error(`Cannot move note: note with ID "${id}" not found`);
821
+ return false;
822
+ }
823
+ // Step 2: Create a copy in the destination folder
824
+ const safeFolder = escapeForAppleScript(destinationFolder);
825
+ const safeContent = originalContent.replace(/"/g, '\\"').replace(/'/g, "'\\''");
826
+ const createCommand = `make new note at folder "${safeFolder}" with properties {body:"${safeContent}"}`;
827
+ const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
828
+ const copyResult = executeAppleScript(script);
829
+ if (!copyResult.success) {
830
+ console.error(`Cannot move note: failed to create in destination folder:`, copyResult.error);
831
+ return false;
832
+ }
833
+ // Step 3: Delete the original by ID
834
+ const deleteCommand = `delete note id "${id}"`;
835
+ const deleteScript = buildAppLevelScript(deleteCommand);
836
+ const deleteResult = executeAppleScript(deleteScript);
837
+ if (!deleteResult.success) {
838
+ console.error(`Note was copied to "${destinationFolder}" but original could not be deleted:`, deleteResult.error);
839
+ return true; // Partial success - note exists in new location
840
+ }
841
+ return true;
842
+ }
712
843
  // ===========================================================================
713
844
  // Account Operations
714
845
  // ===========================================================================
@@ -231,15 +231,18 @@ describe("AppleNotesManager", () => {
231
231
  it("returns array of matching notes with folder info", () => {
232
232
  mockExecuteAppleScript.mockReturnValue({
233
233
  success: true,
234
- output: "Meeting Notes|||Work|||ITEM|||Project Plan|||Notes|||ITEM|||Weekly Review|||Archive",
234
+ output: "Meeting Notes|||x-coredata://ABC/ICNote/p1|||Work|||ITEM|||Project Plan|||x-coredata://ABC/ICNote/p2|||Notes|||ITEM|||Weekly Review|||x-coredata://ABC/ICNote/p3|||Archive",
235
235
  });
236
236
  const results = manager.searchNotes("notes");
237
237
  expect(results).toHaveLength(3);
238
238
  expect(results[0].title).toBe("Meeting Notes");
239
+ expect(results[0].id).toBe("x-coredata://ABC/ICNote/p1");
239
240
  expect(results[0].folder).toBe("Work");
240
241
  expect(results[1].title).toBe("Project Plan");
242
+ expect(results[1].id).toBe("x-coredata://ABC/ICNote/p2");
241
243
  expect(results[1].folder).toBe("Notes");
242
244
  expect(results[2].title).toBe("Weekly Review");
245
+ expect(results[2].id).toBe("x-coredata://ABC/ICNote/p3");
243
246
  expect(results[2].folder).toBe("Archive");
244
247
  });
245
248
  it("returns empty array when no matches found", () => {
@@ -262,7 +265,7 @@ describe("AppleNotesManager", () => {
262
265
  it("searches content when searchContent is true", () => {
263
266
  mockExecuteAppleScript.mockReturnValue({
264
267
  success: true,
265
- output: "Note with keyword|||Notes",
268
+ output: "Note with keyword|||x-coredata://ABC/ICNote/p1|||Notes",
266
269
  });
267
270
  manager.searchNotes("project alpha", true);
268
271
  expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('body contains "project alpha"'));
@@ -270,7 +273,7 @@ describe("AppleNotesManager", () => {
270
273
  it("searches titles when searchContent is false", () => {
271
274
  mockExecuteAppleScript.mockReturnValue({
272
275
  success: true,
273
- output: "Project Alpha Notes|||Notes",
276
+ output: "Project Alpha Notes|||x-coredata://ABC/ICNote/p1|||Notes",
274
277
  });
275
278
  manager.searchNotes("Project Alpha", false);
276
279
  expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('name contains "Project Alpha"'));
@@ -278,13 +281,15 @@ describe("AppleNotesManager", () => {
278
281
  it("identifies notes in Recently Deleted folder", () => {
279
282
  mockExecuteAppleScript.mockReturnValue({
280
283
  success: true,
281
- output: "Old Note|||Recently Deleted|||ITEM|||Active Note|||Notes",
284
+ output: "Old Note|||x-coredata://ABC/ICNote/p1|||Recently Deleted|||ITEM|||Active Note|||x-coredata://ABC/ICNote/p2|||Notes",
282
285
  });
283
286
  const results = manager.searchNotes("note");
284
287
  expect(results).toHaveLength(2);
285
288
  expect(results[0].title).toBe("Old Note");
289
+ expect(results[0].id).toBe("x-coredata://ABC/ICNote/p1");
286
290
  expect(results[0].folder).toBe("Recently Deleted");
287
291
  expect(results[1].title).toBe("Active Note");
292
+ expect(results[1].id).toBe("x-coredata://ABC/ICNote/p2");
288
293
  expect(results[1].folder).toBe("Notes");
289
294
  });
290
295
  it("scopes search to specified account", () => {
@@ -596,8 +601,12 @@ describe("AppleNotesManager", () => {
596
601
  // ---------------------------------------------------------------------------
597
602
  describe("moveNote", () => {
598
603
  it("returns true when move completes successfully", () => {
599
- // Mock sequence: getNoteContent -> createNote -> deleteNote
604
+ // Mock sequence: getNoteDetails -> getNoteContent -> createNote -> deleteNote
600
605
  mockExecuteAppleScript
606
+ .mockReturnValueOnce({
607
+ success: true,
608
+ output: "My Note, x-coredata://ABC/ICNote/p123, date Monday January 1 2024, date Monday January 1 2024, false, false",
609
+ })
601
610
  .mockReturnValueOnce({
602
611
  success: true,
603
612
  output: "<div>Note Title</div><div>Content</div>",
@@ -612,9 +621,9 @@ describe("AppleNotesManager", () => {
612
621
  });
613
622
  const result = manager.moveNote("My Note", "Archive");
614
623
  expect(result).toBe(true);
615
- expect(mockExecuteAppleScript).toHaveBeenCalledTimes(3);
624
+ expect(mockExecuteAppleScript).toHaveBeenCalledTimes(4);
616
625
  });
617
- it("returns false when source note cannot be read", () => {
626
+ it("returns false when source note cannot be found", () => {
618
627
  mockExecuteAppleScript.mockReturnValueOnce({
619
628
  success: false,
620
629
  output: "",
@@ -622,10 +631,14 @@ describe("AppleNotesManager", () => {
622
631
  });
623
632
  const result = manager.moveNote("Missing Note", "Archive");
624
633
  expect(result).toBe(false);
625
- expect(mockExecuteAppleScript).toHaveBeenCalledTimes(1); // Only tried to read
634
+ expect(mockExecuteAppleScript).toHaveBeenCalledTimes(1); // Only tried to get details
626
635
  });
627
636
  it("returns false when copy to destination fails", () => {
628
637
  mockExecuteAppleScript
638
+ .mockReturnValueOnce({
639
+ success: true,
640
+ output: "My Note, x-coredata://ABC/ICNote/p123, date Monday January 1 2024, date Monday January 1 2024, false, false",
641
+ })
629
642
  .mockReturnValueOnce({
630
643
  success: true,
631
644
  output: "<div>Content</div>",
@@ -637,11 +650,15 @@ describe("AppleNotesManager", () => {
637
650
  });
638
651
  const result = manager.moveNote("My Note", "Nonexistent Folder");
639
652
  expect(result).toBe(false);
640
- expect(mockExecuteAppleScript).toHaveBeenCalledTimes(2); // Read + failed create
653
+ expect(mockExecuteAppleScript).toHaveBeenCalledTimes(3); // Details + Read + failed create
641
654
  });
642
655
  it("returns true even if delete fails (note exists in new location)", () => {
643
656
  // This is partial success - note was copied but original couldn't be deleted
644
657
  mockExecuteAppleScript
658
+ .mockReturnValueOnce({
659
+ success: true,
660
+ output: "My Note, x-coredata://ABC/ICNote/p123, date Monday January 1 2024, date Monday January 1 2024, false, false",
661
+ })
645
662
  .mockReturnValueOnce({
646
663
  success: true,
647
664
  output: "<div>Content</div>",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -40,11 +40,11 @@
40
40
  "license": "MIT",
41
41
  "repository": {
42
42
  "type": "git",
43
- "url": "git+https://github.com/sweetrb/mcp-apple-notes.git"
43
+ "url": "git+https://github.com/sweetrb/apple-notes-mcp.git"
44
44
  },
45
- "homepage": "https://github.com/sweetrb/mcp-apple-notes#readme",
45
+ "homepage": "https://github.com/sweetrb/apple-notes-mcp#readme",
46
46
  "bugs": {
47
- "url": "https://github.com/sweetrb/mcp-apple-notes/issues"
47
+ "url": "https://github.com/sweetrb/apple-notes-mcp/issues"
48
48
  },
49
49
  "engines": {
50
50
  "node": ">=20.0.0"