apple-notes-mcp 1.4.1 → 1.4.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
@@ -712,6 +712,22 @@ If installed from source, use this configuration:
712
712
  }
713
713
  ```
714
714
 
715
+ #### Running from a clone in Claude Code (project-scope `.mcp.json`)
716
+
717
+ This repo ships a `.mcp.json` at its root so that, when you run `claude` from inside a clone, the server is registered automatically as a **project-scope** server — no manual config needed. After `npm run build`, just launch Claude Code from the repo directory and approve the server when prompted.
718
+
719
+ The entrypoint is written as:
720
+
721
+ ```json
722
+ "args": ["${CLAUDE_PROJECT_DIR:-.}/build/index.js"]
723
+ ```
724
+
725
+ `CLAUDE_PROJECT_DIR` is the variable Claude Code injects into a project/user-scoped server's environment, and it resolves to the repo root. **You must launch `claude` from inside the repo** for this to work — the bare `.` fallback is only a last resort and is *not* reliable, because it resolves against the launching process's working directory, not the repo.
726
+
727
+ > **Why not `${CLAUDE_PLUGIN_ROOT}`?** `CLAUDE_PLUGIN_ROOT` is set **only** for marketplace plugin installs, never for a project-scope clone, so it can't drive the clone workflow. Conversely, a plugin install can't use `CLAUDE_PROJECT_DIR` (in a plugin, that points at the *user's* project, not the plugin's own directory). Claude Code does **not** support nested defaults like `${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PROJECT_DIR:-.}}`, so a single entrypoint string cannot serve both contexts. The two distribution paths are therefore decoupled: the **plugin** carries its own MCP config in `.claude-plugin/plugin.json` (using `${CLAUDE_PLUGIN_ROOT}`), while the root `.mcp.json` is dedicated to the **clone** workflow (using `${CLAUDE_PROJECT_DIR:-.}`). Because `plugin.json` declares its own `mcpServers`, the plugin does not also auto-load the root `.mcp.json`, so there is no double-registration.
728
+
729
+ > **Heads-up on scope precedence:** project-scope (`.mcp.json`) outranks user-scope. If you *also* have an `apple-notes` entry registered at user scope (e.g. an absolute path in `~/.claude.json`), the project-scope entry wins and the user-scope one is ignored entirely. Pick one — for local development on this repo, the project-scope `.mcp.json` is the intended source. To pin a specific local build instead, register it at **local** scope (`claude mcp add apple-notes -s local -- node /abs/path/build/index.js`), which outranks project scope.
730
+
715
731
  ---
716
732
 
717
733
  ## Full Disk Access for Checklist Features
@@ -757,6 +773,29 @@ All other tools work normally without Full Disk Access. Only checklist state fea
757
773
  | Limited rich formatting | Use `format: "html"` on create/update for headings, lists, bold, code blocks; some complex formatting may not render |
758
774
  | Title matching | Most operations require exact title matches |
759
775
  | Checklist state | Requires Full Disk Access to read done/undone state from the database |
776
+ | Checklist **creation** | Not supported. AppleScript's `body of note` setter strips `<input type="checkbox">` and ignores any checklist-styling CSS class. Apple Notes stores checklist items as a protobuf paragraph style (`style_type=103`) that AppleScript doesn't expose, and the SQLite database is read-only. See [Creating Checklists](#creating-checklists) below for the workaround. |
777
+
778
+ ### Creating Checklists
779
+
780
+ **There is no programmatic way to create a true Apple Notes checklist via AppleScript** — and therefore no way via this MCP server. This is an Apple limitation, not a bug.
781
+
782
+ When a note is created or updated via AppleScript:
783
+
784
+ | You send | What Notes.app actually renders |
785
+ |----------|--------------------------------|
786
+ | `<input type="checkbox"> Item` | `Item` (the `<input>` tag is stripped) |
787
+ | `<ul class="checklist"><li>Item</li></ul>` | A plain bulleted list — the `checklist` class is dropped |
788
+ | Markdown `- [ ] Item` (in `plaintext` mode) | The literal text `- [ ] Item` |
789
+
790
+ Apple Notes stores checklists as a paragraph style (`style_type=103`) inside a gzipped protobuf blob in the `NoteStore.sqlite` database. AppleScript's note `body` interface does not expose paragraph styles, and writing directly to the live database is unsafe.
791
+
792
+ **Workarounds:**
793
+
794
+ 1. **Create the note with bulleted list items, then convert manually in Notes.app.** Select the items and press <kbd>⇧⌘L</kbd> (or **Format → Checklist**). This converts the list in place and the resulting checklist will be readable by `get-checklist-state` and annotated by `get-note-markdown`.
795
+ 2. **Use the Apple Shortcuts app** to script the checklist creation, since Shortcuts can manipulate Notes content at a higher level than AppleScript.
796
+ 3. **Read-only checklist support is fully implemented** — once a checklist exists (created manually or by another app), `get-checklist-state` and `get-note-markdown` will read its done/undone state correctly (with Full Disk Access).
797
+
798
+ If you need to *track* todos programmatically and don't strictly need them rendered as Apple Notes checklist UI, plain markdown-style `- [ ] item` / `- [x] item` lines in a `plaintext` note are a reasonable alternative — they are searchable, human-readable, and can be parsed by downstream tooling.
760
799
 
761
800
  ### Backslash Escaping (Important for AI Agents)
762
801
 
@@ -805,6 +844,12 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
805
844
  - Use `\\` to represent each literal backslash
806
845
  - See "Backslash Escaping" section under Known Limitations
807
846
 
847
+ ### `apple-notes` server fails to connect when run from a clone
848
+ - Launch `claude` from **inside the repo directory** so `CLAUDE_PROJECT_DIR` resolves to the repo root (the bare `.` fallback is unreliable — it points at the launching process's working directory)
849
+ - Run `npm run build` first — the entrypoint is `${CLAUDE_PROJECT_DIR:-.}/build/index.js`, which won't exist until you build
850
+ - Run `claude mcp list` to check for a conflicting `apple-notes` entry at another scope (project-scope outranks user-scope, but local-scope outranks project-scope)
851
+ - Approve the pending project-scope server when Claude Code prompts you
852
+
808
853
  ---
809
854
 
810
855
  ## Development
@@ -838,4 +883,6 @@ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for gui
838
883
 
839
884
  ## Related Projects
840
885
 
841
- - [apple-mail-mcp](https://github.com/sweetrb/apple-mail-mcp) - MCP server for Apple Mail
886
+ - [apple-mail-mcp](https://github.com/sweetrb/apple-mail-mcp) MCP server for Apple Mail
887
+ - [apple-numbers-mcp](https://github.com/sweetrb/apple-numbers-mcp) — MCP server for Apple Numbers spreadsheets
888
+ - [apple-photos-mcp](https://github.com/sweetrb/apple-photos-mcp) — MCP server for Apple Photos
package/build/index.js CHANGED
@@ -26,6 +26,7 @@ import { z } from "zod";
26
26
  import { AppleNotesManager } from "./services/appleNotesManager.js";
27
27
  import { getSyncStatus, withSyncAwarenessSync } from "./utils/syncDetection.js";
28
28
  import { getChecklistItems, hasFullDiskAccess } from "./utils/checklistParser.js";
29
+ import { detectChecklistAttempt } from "./utils/contentWarnings.js";
29
30
  // Read version from package.json to keep it in sync
30
31
  const require = createRequire(import.meta.url);
31
32
  const { version } = require("../package.json");
@@ -112,7 +113,10 @@ const folderNameSchema = {
112
113
  // --- create-note ---
113
114
  server.tool("create-note", {
114
115
  title: z.string().min(1, "Title is required"),
115
- content: z.string().min(1, "Content is required"),
116
+ content: z
117
+ .string()
118
+ .min(1, "Content is required")
119
+ .describe('Note body. AppleScript cannot create true Apple Notes checklists — `<input type="checkbox">`, checklist CSS classes, and markdown `- [ ]` lines do not render as checkable items. To produce a checklist, create the note with a plain `<ul>` or `- ` list and convert it in Notes.app with ⇧⌘L.'),
116
120
  format: z
117
121
  .enum(["plaintext", "html"])
118
122
  .optional()
@@ -129,7 +133,8 @@ server.tool("create-note", {
129
133
  if (!note) {
130
134
  return errorResponse(`Failed to create note "${title}". Check that Notes.app is configured and accessible.`);
131
135
  }
132
- return successResponse(`Note created: "${note.title}" [id: ${note.id}]`);
136
+ const checklistWarning = detectChecklistAttempt(content) ?? "";
137
+ return successResponse(`Note created: "${note.title}" [id: ${note.id}]${checklistWarning}`);
133
138
  }, "Error creating note"));
134
139
  // --- search-notes ---
135
140
  server.tool("search-notes", {
@@ -261,7 +266,10 @@ server.tool("update-note", {
261
266
  id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
262
267
  title: z.string().optional().describe("Current note title (use id instead when available)"),
263
268
  newTitle: z.string().optional().describe("New title for the note"),
264
- newContent: z.string().min(1, "New content is required"),
269
+ newContent: z
270
+ .string()
271
+ .min(1, "New content is required")
272
+ .describe("New note body. AppleScript cannot produce true Apple Notes checklists; checkbox inputs and `- [ ]` markdown do not render as checkable items. Use a plain list and convert in Notes.app with ⇧⌘L."),
265
273
  format: z
266
274
  .enum(["plaintext", "html"])
267
275
  .optional()
@@ -291,7 +299,8 @@ server.tool("update-note", {
291
299
  const sharedWarning = note.shared
292
300
  ? "\n\n⚠️ This note is shared with collaborators. Your changes will be visible to them."
293
301
  : "";
294
- return successResponse(`Note updated: "${displayTitle}"${sharedWarning}`);
302
+ const checklistWarning = detectChecklistAttempt(newContent) ?? "";
303
+ return successResponse(`Note updated: "${displayTitle}"${sharedWarning}${checklistWarning}`);
295
304
  }
296
305
  // Fall back to title-based update
297
306
  if (!title) {
@@ -314,7 +323,8 @@ server.tool("update-note", {
314
323
  const sharedWarning = note.shared
315
324
  ? "\n\n⚠️ This note is shared with collaborators. Your changes will be visible to them."
316
325
  : "";
317
- return successResponse(`Note updated: "${finalTitle}"${sharedWarning}`);
326
+ const checklistWarning = detectChecklistAttempt(newContent) ?? "";
327
+ return successResponse(`Note updated: "${finalTitle}"${sharedWarning}${checklistWarning}`);
318
328
  }, "Error updating note"));
319
329
  // --- delete-note ---
320
330
  server.tool("delete-note", {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Content Warnings for create-note / update-note
3
+ *
4
+ * Detects content patterns that look like the user is trying to do something
5
+ * Apple Notes via AppleScript cannot actually render — so the response can
6
+ * carry a clear warning instead of silently producing a broken note.
7
+ *
8
+ * @module utils/contentWarnings
9
+ */
10
+ /**
11
+ * Detects checklist-like syntax in note content.
12
+ *
13
+ * Apple Notes checklists are a paragraph style stored in a protobuf blob in
14
+ * the NoteStore SQLite database. AppleScript's `body of note` setter does not
15
+ * expose paragraph styles: `<input type="checkbox">` is stripped, a
16
+ * `class="checklist"` on `<ul>` is dropped, and markdown `- [ ]` lines in
17
+ * `plaintext` mode arrive as literal text. There is no input that produces a
18
+ * real checklist.
19
+ *
20
+ * @param content - The user-supplied note body (HTML or plaintext)
21
+ * @returns A user-facing warning string, or null when no checklist-like
22
+ * patterns are present
23
+ */
24
+ export function detectChecklistAttempt(content) {
25
+ if (!content)
26
+ return null;
27
+ // HTML checkbox input — `<input type="checkbox" ...>` in either quoting style.
28
+ const htmlCheckbox = /<input\b[^>]*\btype\s*=\s*["']checkbox["']/i.test(content);
29
+ // Markdown-style checklist: lines starting with optional whitespace, then
30
+ // `-` or `*`, a space, and `[ ]` / `[x]` / `[X]`.
31
+ const markdownCheckbox = /^[ \t]*[-*]\s+\[[ xX]\]/m.test(content);
32
+ // CSS class hint — some clients try `<ul class="checklist">` or
33
+ // `<li class="todo">`. AppleScript drops these classes too.
34
+ const checklistClass = /class\s*=\s*["'][^"']*\b(?:checklist|todo)\b/i.test(content);
35
+ if (!htmlCheckbox && !markdownCheckbox && !checklistClass)
36
+ return null;
37
+ return ("\n\n⚠️ Your content looks like a checklist, but Apple Notes checklists " +
38
+ 'cannot be created via AppleScript — `<input type="checkbox">` is ' +
39
+ "stripped, checklist CSS classes are dropped, and markdown `- [ ]` lines " +
40
+ "arrive as literal text. The note was created with the surrounding " +
41
+ "structure (list items or paragraphs) intact. To convert it to a real " +
42
+ "Apple Notes checklist, open the note, select the items, and press " +
43
+ "⇧⌘L (Format → Checklist).");
44
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Tests for the content-warning detectors.
3
+ */
4
+ import { describe, it, expect } from "vitest";
5
+ import { detectChecklistAttempt } from "./contentWarnings.js";
6
+ describe("detectChecklistAttempt", () => {
7
+ it("returns null for plain text without checklist syntax", () => {
8
+ expect(detectChecklistAttempt("Just a regular note.")).toBeNull();
9
+ });
10
+ it("returns null for empty content", () => {
11
+ expect(detectChecklistAttempt("")).toBeNull();
12
+ });
13
+ it("returns null for HTML lists that are not checklists", () => {
14
+ expect(detectChecklistAttempt("<ul><li>Apple</li><li>Banana</li></ul>")).toBeNull();
15
+ });
16
+ it('warns on <input type="checkbox"> (double-quoted)', () => {
17
+ const w = detectChecklistAttempt('<input type="checkbox"> Buy milk');
18
+ expect(w).not.toBeNull();
19
+ expect(w).toContain("⚠️");
20
+ expect(w).toContain("⇧⌘L");
21
+ });
22
+ it("warns on <input type='checkbox'> (single-quoted)", () => {
23
+ expect(detectChecklistAttempt("<input type='checkbox'> Buy milk")).not.toBeNull();
24
+ });
25
+ it("warns on <input> with extra attributes before type", () => {
26
+ expect(detectChecklistAttempt('<input id="x" type="checkbox"> Item')).not.toBeNull();
27
+ });
28
+ it('warns on <INPUT TYPE="CHECKBOX"> (case-insensitive)', () => {
29
+ expect(detectChecklistAttempt('<INPUT TYPE="CHECKBOX"> Item')).not.toBeNull();
30
+ });
31
+ it("warns on markdown `- [ ]` syntax", () => {
32
+ expect(detectChecklistAttempt("- [ ] todo 1\n- [x] done 1")).not.toBeNull();
33
+ });
34
+ it("warns on markdown `* [ ]` syntax", () => {
35
+ expect(detectChecklistAttempt("* [ ] todo 1")).not.toBeNull();
36
+ });
37
+ it("warns on markdown checklist with leading whitespace", () => {
38
+ expect(detectChecklistAttempt(" - [ ] indented todo")).not.toBeNull();
39
+ });
40
+ it('warns on <ul class="checklist">', () => {
41
+ expect(detectChecklistAttempt('<ul class="checklist"><li>a</li></ul>')).not.toBeNull();
42
+ });
43
+ it('warns on <li class="todo">', () => {
44
+ expect(detectChecklistAttempt('<ul><li class="todo">a</li></ul>')).not.toBeNull();
45
+ });
46
+ it("does not warn on the word 'checklist' in prose", () => {
47
+ expect(detectChecklistAttempt("My checklist of things to do tomorrow.")).toBeNull();
48
+ });
49
+ it("does not warn on a literal `[ ]` not at start of a list line", () => {
50
+ expect(detectChecklistAttempt("The brackets [ ] are not a checklist.")).toBeNull();
51
+ });
52
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
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",
@@ -25,6 +25,7 @@
25
25
  "format": "prettier --write src",
26
26
  "format:check": "prettier --check src",
27
27
  "typecheck": "tsc --noEmit",
28
+ "version": "node -e \"const p=require('./package.json'); const f='.claude-plugin/plugin.json'; const c=JSON.parse(require('fs').readFileSync(f,'utf8')); c.version=p.version; require('fs').writeFileSync(f,JSON.stringify(c,null,2)+'\\n')\" && git add .claude-plugin/plugin.json",
28
29
  "prepublishOnly": "npm run lint && npm run test && npm run build",
29
30
  "prepare": "husky && npm run build"
30
31
  },