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.
|
|
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",
|
|
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
|
-
|
|
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
|
|
194
|
-
|
|
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",
|
|
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
|
-
|
|
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 = `
|
|
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 = `
|
|
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
|
-
//
|
|
283
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
703
|
-
const
|
|
704
|
-
|
|
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(
|
|
624
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledTimes(4);
|
|
616
625
|
});
|
|
617
|
-
it("returns false when source note cannot be
|
|
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
|
|
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(
|
|
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
|
|
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/
|
|
43
|
+
"url": "git+https://github.com/sweetrb/apple-notes-mcp.git"
|
|
44
44
|
},
|
|
45
|
-
"homepage": "https://github.com/sweetrb/
|
|
45
|
+
"homepage": "https://github.com/sweetrb/apple-notes-mcp#readme",
|
|
46
46
|
"bugs": {
|
|
47
|
-
"url": "https://github.com/sweetrb/
|
|
47
|
+
"url": "https://github.com/sweetrb/apple-notes-mcp/issues"
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
50
|
"node": ">=20.0.0"
|