apple-notes-mcp 1.4.0 → 1.4.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/README.md CHANGED
@@ -812,7 +812,7 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
812
812
  ```bash
813
813
  npm install # Install dependencies
814
814
  npm run build # Compile TypeScript
815
- npm test # Run test suite (310 tests)
815
+ npm test # Run test suite (313 tests)
816
816
  npm run lint # Check code style
817
817
  npm run format # Format code
818
818
  ```
package/build/index.js CHANGED
@@ -454,10 +454,16 @@ server.tool("list-folders", {
454
454
  return successResponse(`Found ${folders.length} folders${acct}:\n${folderList}${syncNote}`);
455
455
  }, "Error listing folders"));
456
456
  // --- create-folder ---
457
- server.tool("create-folder", folderNameSchema, withErrorHandling(({ name, account }) => {
457
+ server.tool("create-folder", {
458
+ name: z
459
+ .string()
460
+ .min(1, "Folder name is required")
461
+ .describe('Folder name or nested path separated by "/". E.g., "Retro Tech/PC/CPUs" creates all intermediate folders. Existing segments are skipped.'),
462
+ account: z.string().optional().describe("Account name (defaults to iCloud)"),
463
+ }, withErrorHandling(({ name, account }) => {
458
464
  const folder = notesManager.createFolder(name, account);
459
465
  if (!folder) {
460
- return errorResponse(`Failed to create folder "${name}". It may already exist.`);
466
+ return errorResponse(`Failed to create folder "${name}".`);
461
467
  }
462
468
  return successResponse(`Folder created: "${folder.name}"`);
463
469
  }, "Error creating folder"));
@@ -539,11 +539,11 @@ export class AppleNotesManager {
539
539
  let createCommand;
540
540
  if (folder) {
541
541
  // Create note in specific folder (supports nested paths like "Work/Clients")
542
+ // Note: We avoid `set newNote` + `return id of newNote` because AppleScript
543
+ // fails to resolve the note reference in deeply nested folder contexts (-1728).
544
+ // The implicit return from `make new note` includes the ID which we parse.
542
545
  const folderRef = buildFolderReference(folder);
543
- createCommand = `
544
- set newNote to make new note at ${folderRef} with properties {body:"${safeBody}"}
545
- return id of newNote
546
- `;
546
+ createCommand = `make new note at ${folderRef} with properties {body:"${safeBody}"}`;
547
547
  }
548
548
  else {
549
549
  // Create note in default location
@@ -1194,16 +1194,52 @@ export class AppleNotesManager {
1194
1194
  */
1195
1195
  createFolder(name, account) {
1196
1196
  const targetAccount = this.resolveAccount(account);
1197
- const safeName = escapeForAppleScript(name);
1198
- const createCommand = `make new folder with properties {name:"${safeName}"}`;
1199
- const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
1200
- const result = executeAppleScript(script);
1201
- if (!result.success) {
1202
- console.error(`Failed to create folder "${name}":`, result.error);
1197
+ const parts = splitFolderPath(name);
1198
+ if (parts.length === 0) {
1199
+ console.error(`Invalid folder name: "${name}"`);
1203
1200
  return null;
1204
1201
  }
1205
- // Extract the folder ID from the response
1206
- const folderId = extractCoreDataId(result.output, "folder");
1202
+ // Create each segment of the path, checking existence first to avoid duplicates.
1203
+ // For "A/B/C": ensure "A" exists, then "A/B", then "A/B/C".
1204
+ for (let i = 0; i < parts.length; i++) {
1205
+ const currentPath = parts
1206
+ .slice(0, i + 1)
1207
+ .map((p) => escapeFolderName(p))
1208
+ .join("/");
1209
+ const currentRef = buildFolderReference(currentPath);
1210
+ // Check if this folder already exists
1211
+ const checkScript = buildAccountScopedScript({ account: targetAccount }, `return id of ${currentRef}`);
1212
+ const checkResult = executeAppleScript(checkScript);
1213
+ if (checkResult.success) {
1214
+ // Folder exists, move to next segment
1215
+ continue;
1216
+ }
1217
+ // Folder doesn't exist — create it
1218
+ const segmentName = escapeForAppleScript(parts[i]);
1219
+ let createCommand;
1220
+ if (i === 0) {
1221
+ createCommand = `make new folder with properties {name:"${segmentName}"}`;
1222
+ }
1223
+ else {
1224
+ const parentPath = parts
1225
+ .slice(0, i)
1226
+ .map((p) => escapeFolderName(p))
1227
+ .join("/");
1228
+ const parentRef = buildFolderReference(parentPath);
1229
+ createCommand = `make new folder at ${parentRef} with properties {name:"${segmentName}"}`;
1230
+ }
1231
+ const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
1232
+ const result = executeAppleScript(script);
1233
+ if (!result.success) {
1234
+ console.error(`Failed to create folder "${name}":`, result.error);
1235
+ return null;
1236
+ }
1237
+ }
1238
+ // Get the ID of the final (deepest) folder
1239
+ const fullRef = buildFolderReference(name);
1240
+ const idScript = buildAccountScopedScript({ account: targetAccount }, `return id of ${fullRef}`);
1241
+ const idResult = executeAppleScript(idScript);
1242
+ const folderId = idResult.success ? extractCoreDataId(idResult.output, "folder") : "";
1207
1243
  return {
1208
1244
  id: folderId,
1209
1245
  name,
@@ -1047,7 +1047,16 @@ describe("AppleNotesManager", () => {
1047
1047
  });
1048
1048
  describe("createFolder", () => {
1049
1049
  it("returns Folder object on success", () => {
1050
- mockExecuteAppleScript.mockReturnValue({
1050
+ mockExecuteAppleScript
1051
+ // Check existence — folder doesn't exist
1052
+ .mockReturnValueOnce({ success: false, output: "", error: "Can't get folder" })
1053
+ // Create the folder
1054
+ .mockReturnValueOnce({
1055
+ success: true,
1056
+ output: "folder id x-coredata://ABC123/ICFolder/p456",
1057
+ })
1058
+ // Get ID of created folder
1059
+ .mockReturnValueOnce({
1051
1060
  success: true,
1052
1061
  output: "folder id x-coredata://ABC123/ICFolder/p456",
1053
1062
  });
@@ -1056,15 +1065,82 @@ describe("AppleNotesManager", () => {
1056
1065
  expect(result?.name).toBe("New Project");
1057
1066
  expect(result?.id).toBe("x-coredata://ABC123/ICFolder/p456");
1058
1067
  });
1059
- it("returns null on failure", () => {
1060
- mockExecuteAppleScript.mockReturnValue({
1068
+ it("returns existing folder without creating duplicate", () => {
1069
+ mockExecuteAppleScript
1070
+ // Check existence — folder already exists
1071
+ .mockReturnValueOnce({
1072
+ success: true,
1073
+ output: "x-coredata://ABC123/ICFolder/p789",
1074
+ })
1075
+ // Get ID of existing folder
1076
+ .mockReturnValueOnce({
1077
+ success: true,
1078
+ output: "folder id x-coredata://ABC123/ICFolder/p789",
1079
+ });
1080
+ const result = manager.createFolder("Existing Folder");
1081
+ expect(result).not.toBeNull();
1082
+ expect(result?.name).toBe("Existing Folder");
1083
+ expect(result?.id).toBe("x-coredata://ABC123/ICFolder/p789");
1084
+ // Should only have 2 calls (check + get ID), no create call
1085
+ expect(mockExecuteAppleScript).toHaveBeenCalledTimes(2);
1086
+ });
1087
+ it("returns null on genuine failure", () => {
1088
+ mockExecuteAppleScript
1089
+ // Check existence — doesn't exist
1090
+ .mockReturnValueOnce({ success: false, output: "", error: "Can't get folder" })
1091
+ // Create fails
1092
+ .mockReturnValueOnce({
1061
1093
  success: false,
1062
1094
  output: "",
1063
- error: "Folder already exists",
1095
+ error: "Permission denied",
1064
1096
  });
1065
- const result = manager.createFolder("Existing Folder");
1097
+ const result = manager.createFolder("Restricted Folder");
1066
1098
  expect(result).toBeNull();
1067
1099
  });
1100
+ it("creates nested folder path", () => {
1101
+ mockExecuteAppleScript
1102
+ // Check "Retro Tech" — doesn't exist
1103
+ .mockReturnValueOnce({ success: false, output: "", error: "Can't get folder" })
1104
+ // Create "Retro Tech"
1105
+ .mockReturnValueOnce({ success: true, output: "folder id x-coredata://A/ICFolder/p1" })
1106
+ // Check "Retro Tech/PC" — doesn't exist
1107
+ .mockReturnValueOnce({ success: false, output: "", error: "Can't get folder" })
1108
+ // Create "PC" inside "Retro Tech"
1109
+ .mockReturnValueOnce({ success: true, output: "folder id x-coredata://A/ICFolder/p2" })
1110
+ // Check "Retro Tech/PC/CPUs" — doesn't exist
1111
+ .mockReturnValueOnce({ success: false, output: "", error: "Can't get folder" })
1112
+ // Create "CPUs" inside "Retro Tech/PC"
1113
+ .mockReturnValueOnce({ success: true, output: "folder id x-coredata://A/ICFolder/p3" })
1114
+ // Get ID of final folder
1115
+ .mockReturnValueOnce({ success: true, output: "folder id x-coredata://A/ICFolder/p3" });
1116
+ const result = manager.createFolder("Retro Tech/PC/CPUs");
1117
+ expect(result).not.toBeNull();
1118
+ expect(result?.name).toBe("Retro Tech/PC/CPUs");
1119
+ expect(result?.id).toBe("x-coredata://A/ICFolder/p3");
1120
+ // Verify the create commands (calls at index 1, 3, 5)
1121
+ const calls = mockExecuteAppleScript.mock.calls;
1122
+ expect(calls[1][0]).toContain('make new folder with properties {name:"Retro Tech"}');
1123
+ expect(calls[3][0]).toContain('make new folder at folder "Retro Tech" with properties {name:"PC"}');
1124
+ expect(calls[5][0]).toContain('make new folder at folder "PC" of folder "Retro Tech" with properties {name:"CPUs"}');
1125
+ });
1126
+ it("skips existing intermediate folders in nested path", () => {
1127
+ mockExecuteAppleScript
1128
+ // Check "Retro Tech" — exists
1129
+ .mockReturnValueOnce({ success: true, output: "x-coredata://A/ICFolder/p1" })
1130
+ // Check "Retro Tech/PC" — doesn't exist
1131
+ .mockReturnValueOnce({ success: false, output: "", error: "Can't get folder" })
1132
+ // Create "PC" inside "Retro Tech"
1133
+ .mockReturnValueOnce({ success: true, output: "folder id x-coredata://A/ICFolder/p2" })
1134
+ // Get ID of final folder
1135
+ .mockReturnValueOnce({ success: true, output: "folder id x-coredata://A/ICFolder/p2" });
1136
+ const result = manager.createFolder("Retro Tech/PC");
1137
+ expect(result).not.toBeNull();
1138
+ expect(result?.name).toBe("Retro Tech/PC");
1139
+ // No create call for "Retro Tech" — only for "PC"
1140
+ const createCalls = mockExecuteAppleScript.mock.calls.filter((c) => c[0].includes("make new folder"));
1141
+ expect(createCalls).toHaveLength(1);
1142
+ expect(createCalls[0][0]).toContain('name:"PC"');
1143
+ });
1068
1144
  });
1069
1145
  describe("deleteFolder", () => {
1070
1146
  it("returns true on successful deletion", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.4.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",