apple-notes-mcp 2.8.14 → 2.8.16

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
@@ -59,18 +59,36 @@ This method also installs a **skill** that teaches Claude when and how to use Ap
59
59
  On the first tool call, macOS shows an Automation permission prompt ("Claude" wants access to control "Notes") — click **OK**. Optionally, grant **Full Disk Access** to the app that launches the server to enable the database-backed tools (`get-checklist-state`, `get-note-metadata`, `get-note-link`, checklist annotations in `get-note-markdown`, and full `get-sync-status` detail); see the [Full Disk Access Setup Guide](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md). The rest of the server is pure AppleScript and works without it.
60
60
 
61
61
  Native tag, checklist, table, pin, and rich append operations use two packaged
62
- Apple Shortcuts. Run the explicit setup once:
62
+ Apple Shortcuts. A third, `Apple Notes MCP - Create Markdown Note`, is optional:
63
+ only `create-note`'s `format: "markdown"` needs it, and only on macOS 26 or
64
+ later. Run the explicit setup once:
63
65
 
64
66
  ```bash
65
67
  npx -y apple-notes-mcp setup
66
68
  ```
67
69
 
68
70
  The command checks existing installations and opens only missing signed
69
- workflows. Confirm **Add Shortcut** in each macOS window, then verify with
70
- `npx -y apple-notes-mcp setup --check` or the MCP `doctor` tool. macOS does not
71
+ workflows; it opens the optional Create Markdown Note bridge only on macOS 26 or
72
+ later. Confirm **Add Shortcut** in each macOS window, then verify with
73
+ `npx -y apple-notes-mcp setup --check` or the MCP `doctor` tool. `setup --check`
74
+ reports ready and `doctor` reports ok once the two required bridges are
75
+ installed; both list the optional bridge's status separately. macOS does not
71
76
  support silent Shortcut import, so merely connecting an MCP client never opens
72
77
  setup windows or bypasses these confirmations.
73
78
 
79
+ After install **and after every upgrade**, open Shortcuts.app and run each
80
+ installed bridge — `Apple Notes MCP - Native Tags`,
81
+ `Apple Notes MCP - Background Operations v5` and, if you installed it, the
82
+ optional `Apple Notes MCP - Create Markdown Note` — once in the foreground, choosing
83
+ **Always Allow** when Shortcuts asks for permission. The Create Markdown Note
84
+ bridge stops before reaching Notes when run with no input, so start its run
85
+ with the request in [`shortcuts/README.md`](shortcuts/README.md). The server runs these
86
+ Shortcuts in the background, where Shortcuts cannot display a first-run consent
87
+ prompt: an unanswered one stalls every native write on that bridge until it
88
+ times out, while `doctor` still reports the bridge installed. Quitting or
89
+ relaunching Shortcuts.app or Notes.app does not clear it; the foreground run
90
+ does, once per bridge.
91
+
74
92
  ### Using the Codex Marketplace
75
93
 
76
94
  The same plugin is available for Codex. Add the marketplace and install the plugin:
@@ -192,10 +210,10 @@ Creates a new note in Apple Notes.
192
210
  |-----------|------|----------|-------------|
193
211
  | `title` | string | Yes | The title of the note. Automatically prepended as `<h1>` — do NOT include the title in `content` |
194
212
  | `content` | string | Yes | The body content of the note (do not repeat the title here) |
195
- | `tags` | string[] | No | Returned-only metadata — **NOT written to Notes.app**. Apple Notes tags can't be set via AppleScript, so values passed here are echoed back in the response but do not appear on the created note. Use inline `#hashtags` in `content` instead (Notes.app turns those into real tags) |
213
+ | `tags` | string[] | No | Returned-only metadata — **NOT written to Notes.app**. Apple Notes tags can't be set via AppleScript, so values passed here are echoed back in the response but do not appear on the created note. Use inline `#hashtags` in `content` instead (Notes.app turns those into real tags). Refused with `format: "markdown"` |
196
214
  | `folder` | string | No | Folder to create the note in. Supports nested paths like `"Work/Clients"`. **The folder must already exist** — create it first with [`create-folder`](#create-folder). Defaults to account root |
197
215
  | `account` | string | No | Account name (defaults to Notes.app's default account; matched exactly or by a *unique* prefix — an ambiguous prefix is refused). Must be an account Notes.app already has configured — see [`list-accounts`](#list-accounts) |
198
- | `format` | string | No | Content format: `"plaintext"` (default) or `"html"`. In both formats, the title is automatically prepended as `<h1>`. In plaintext mode, newlines become `<br>`, tabs become `<br>`, and backslashes are preserved as HTML entities |
216
+ | `format` | string | No | Content format: `"plaintext"` (default), `"html"`, or `"markdown"`. In all formats, the title is automatically prepended as the note's title line. In plaintext mode, newlines become `<br>`, tabs become `<br>`, and backslashes are preserved as HTML entities. `"markdown"` produces real Title/Heading/Subheading styles through a Shortcut; see [Markdown notes](#markdown-notes) |
199
217
 
200
218
  **Example (tagged with inline hashtags):**
201
219
  ```json
@@ -225,13 +243,63 @@ Creates a new note in Apple Notes.
225
243
 
226
244
  > **Note:** The title is automatically prepended as `<h1>` in both plaintext and HTML formats. Do not include a `<h1>` title tag in the `content` parameter, or the title will appear twice.
227
245
 
228
- > **Known limitation:** `create-note` sets the note body directly via AppleScript's
229
- > `body` property, which does not apply real Notes paragraph styles for interior
230
- > content — an `<h2>`/`<h3>` tag or a `<span style="font-size: …px">` heading span
231
- > in `content` renders as plain bold, styled text, not an actual Heading or
232
- > Subheading ([#172](https://github.com/sweetrb/apple-notes-mcp/issues/172)). Real
233
- > Heading/Subheading styles can currently only be added to a note that already
234
- > exists, with [`append-native`](#append-native)'s `format: "markdown"`.
246
+ > **Known limitation:** with `"plaintext"` or `"html"`, `create-note` sets the note
247
+ > body directly via AppleScript's `body` property, which does not apply real Notes
248
+ > paragraph styles for interior content — an `<h2>`/`<h3>` tag or a
249
+ > `<span style="font-size: …px">` heading span in `content` renders as plain bold,
250
+ > styled text, not an actual Heading or Subheading
251
+ > ([#172](https://github.com/sweetrb/apple-notes-mcp/issues/172)). Use
252
+ > `format: "markdown"` for real headings in a new note, or
253
+ > [`append-native`](#append-native)'s `format: "markdown"` on a note that already
254
+ > exists.
255
+
256
+ ##### Markdown notes
257
+
258
+ `format: "markdown"` creates the note with Notes' own Markdown importer (the
259
+ Create Note action's "Interpret as Markdown" option, macOS 26+), run through the
260
+ packaged `Apple Notes MCP - Create Markdown Note` Shortcut. `#`, `##` and `###`
261
+ become real Title, Heading and Subheading styles, and the note starts with the
262
+ title line, with no seed line.
263
+
264
+ ```json
265
+ {
266
+ "title": "Project Plan",
267
+ "content": "## Goals\n\n- Ship the beta\n- Collect feedback\n\n### Links\n\n[Tracker](https://example.com/tracker)",
268
+ "format": "markdown",
269
+ "folder": "Work"
270
+ }
271
+ ```
272
+
273
+ - Notes interprets Markdown only in an iCloud account. The note is created in the
274
+ iCloud account's default folder, then moved to `folder` in that account.
275
+ `folder` must already exist and is checked before anything is created. If it
276
+ exists only in another account, the note is still created and verified in the
277
+ iCloud default folder, and the error names that account and the note's id so
278
+ you can create the folder there and `move-note` it instead of creating the
279
+ note again. `account` is refused with this format.
280
+ - `tags` are refused with this format. Create the note without them, then add
281
+ native tags to the returned id with [`add-native-tags`](#add-native-tags).
282
+ - `content` accepts the same bounded subset as `append-native`'s Markdown:
283
+ `#`/`##`/`###` headings, flat lists, `**bold**`, `*italic*` and inline links.
284
+ It also refuses Markdown that Notes would rewrite and the server could not
285
+ verify: `_` emphasis (underscores inside a word, as in `snake_case`, and
286
+ inside a link destination, as in `[docs](https://example.com/_next/static)`,
287
+ are fine), backslash escapes, character references such as `&amp;`, `---` or
288
+ `===` lines, indented headings or list items, `1)` lists, closing `#`s, and
289
+ formatting inside link labels. Content that needs one of these literally, such
290
+ as a `/_next` path or a literal `\*`, has no Markdown form here: use
291
+ `format: "html"` for that note (or `append-native` with `format: "html"` on an
292
+ existing one), which keeps the characters but not the Heading and Subheading
293
+ styles. Markdown punctuation in `title` is escaped, so the title stays literal.
294
+ - The server finds the new note among the notes added to the default folder
295
+ during the run by verifying each one's visible text, heading levels and links
296
+ by exact-ID readback, and moves it only after exactly one verifies. On any
297
+ uncertain result it names the note (or says to search for the title) and
298
+ never retries.
299
+ - It is gated like the other native operations; `get-capabilities` reports it
300
+ as `create-note-markdown`. The Create Markdown Note Shortcut is optional and
301
+ needed only for this format (macOS 26+); install and approve it as described in
302
+ [`shortcuts/README.md`](shortcuts/README.md).
235
303
 
236
304
  **Returns:** Confirmation message with note title and ID. Save the ID for subsequent operations like `update-note`, `delete-note`, etc.
237
305
 
@@ -347,7 +415,10 @@ Access.
347
415
  #### `native-tags-status`
348
416
 
349
417
  Checks whether exactly one configured Native Tags Shortcut is installed. An
350
- installed workflow may still need macOS permission on its first execution.
418
+ installed workflow may still need macOS permission on its first execution, and
419
+ installation does not show whether that consent was given: run it once in the
420
+ foreground in Shortcuts.app and choose **Always Allow** after install or upgrade
421
+ (see [Troubleshooting](#native-writes-time-out-or-report-an-uncertain-outcome)).
351
422
 
352
423
  ---
353
424
 
@@ -1056,10 +1127,15 @@ Lists all notes shared with collaborators.
1056
1127
  ### Native background operations
1057
1128
 
1058
1129
  Install the signed workflows once as described in
1059
- [`shortcuts/README.md`](shortcuts/README.md). Native writes never use UI
1060
- automation or write directly to the Notes database. Every operation requires an
1061
- exact note ID, a fresh `expectedContentHash`, and a distinctive existing
1062
- `scopeText` so the Shortcut and server can independently resolve the same note.
1130
+ [`shortcuts/README.md`](shortcuts/README.md), then run each once in the
1131
+ foreground in Shortcuts.app and choose **Always Allow** — again after every
1132
+ upgrade — so no first-run consent prompt is left for a background run that
1133
+ cannot display it. Native writes never use UI
1134
+ automation or write directly to the Notes database. Every operation on an
1135
+ existing note requires an exact note ID, a fresh `expectedContentHash`, and a
1136
+ distinctive existing `scopeText` so the Shortcut and server can independently
1137
+ resolve the same note. Creating a note from Markdown is
1138
+ [`create-note`](#create-note) with `format: "markdown"`.
1063
1139
 
1064
1140
  #### `get-capabilities`
1065
1141
 
@@ -1079,9 +1155,14 @@ subset as `format: "html"`'s Markdown-shaped content (`#`/`##`/`###` headings,
1079
1155
  flat lists, emphasis, and inline links) but is sent through Notes' own native
1080
1156
  Markdown importer rather than converted to HTML first, so `#`/`##`/`###`
1081
1157
  produce real Title/Heading/Subheading — Notes' HTML importer only
1082
- distinguishes two heading levels and renders `###` the same as `##`. A
1158
+ distinguishes two heading levels and renders `###` the same as `##`. Markdown
1159
+ that the importer would rewrite, so the appended text could not be verified, is
1160
+ refused before anything is written; the list is under
1161
+ [Markdown notes](#markdown-notes). A
1083
1162
  transport failure names the Shortcut it was waiting on, so a missing or
1084
- unapproved bridge is identified rather than guessed at.
1163
+ unapproved bridge is identified rather than guessed at; a timeout also says it
1164
+ may be an unanswered first-run consent prompt and names the Shortcut to run once
1165
+ in the foreground.
1085
1166
 
1086
1167
  #### `create-checklist-item`
1087
1168
 
@@ -1396,6 +1477,12 @@ In a JSON string literal the two characters `\\` denote **one** literal backslas
1396
1477
  - Go to System Settings > Privacy & Security > Automation
1397
1478
  - Ensure your terminal/Claude has permission to control Notes
1398
1479
 
1480
+ ### Native writes time out or report an uncertain outcome
1481
+ - Symptom: `add-native-tags`, `set-note-pinned`, `append-native` or another native write fails with "Shortcuts timed out waiting for …", "Operation outcome uncertain" or "readback was not verified", while `doctor` and `get-capabilities` report the bridges installed
1482
+ - Common cause, especially right after install or upgrade: the bridge Shortcut is waiting on a first-run consent prompt. The server runs it in the background, where Shortcuts cannot display that prompt, so the run stalls until it times out
1483
+ - Fix: open Shortcuts.app, run the Shortcut the error names (`Apple Notes MCP - Native Tags`, `Apple Notes MCP - Background Operations v5` or `Apple Notes MCP - Create Markdown Note`) once in the foreground and choose **Always Allow**. Each bridge needs this once; quitting or relaunching Shortcuts.app or Notes.app does not clear it
1484
+ - Read the exact note before retrying — the write may have landed
1485
+
1399
1486
  ### "Note not found"
1400
1487
  - Note titles must match exactly (case-sensitive)
1401
1488
  - Check if the note is in a different account
package/build/index.js CHANGED
@@ -42736,6 +42736,14 @@ import { execFileSync as execFileSync6 } from "node:child_process";
42736
42736
  import { mkdtempSync as mkdtempSync2, writeFileSync, rmSync as rmSync2 } from "node:fs";
42737
42737
  import { tmpdir as tmpdir2 } from "node:os";
42738
42738
  import { join as join7 } from "node:path";
42739
+
42740
+ // src/services/shortcutConsent.ts
42741
+ function shortcutConsentHint(shortcut) {
42742
+ const target = shortcut ? `"${shortcut}"` : "the bridge Shortcut";
42743
+ return `This may be an unanswered first-run Shortcuts consent prompt, which a background run cannot display: run ${target} once in the foreground in Shortcuts.app and choose Always Allow.`;
42744
+ }
42745
+
42746
+ // src/services/nativeTags.ts
42739
42747
  var NATIVE_TAGS_SHORTCUT = "Apple Notes MCP - Native Tags";
42740
42748
  function normalizeNativeTags(tags) {
42741
42749
  if (!tags.length || tags.length > 100) throw new Error("Provide between 1 and 100 tags");
@@ -42790,7 +42798,7 @@ function addNativeTags(request, deps) {
42790
42798
  };
42791
42799
  if (after.title !== before.title || allTags.some((tag) => !after.rich.nativeTags.includes(tag)) || textWithoutTags(before.rich.text) !== textWithoutTags(after.rich.text) || linkSignature(before.rich.links) !== linkSignature(after.rich.links) || before.rich.nativeObjectIds.some((id2) => !after.rich.nativeObjectIds.includes(id2)) || before.rich.hasChecklist !== after.rich.hasChecklist) {
42792
42800
  throw new Error(
42793
- "Shortcuts ran, but exact-ID tags/text/links readback was not verified. Read the note before any retry"
42801
+ transportWarning ? `Shortcuts did not complete cleanly, and exact-ID tags/text/links readback was not verified. Read the note before any retry. ${transportWarning}` : "Shortcuts ran, but exact-ID tags/text/links readback was not verified. Read the note before any retry"
42794
42802
  );
42795
42803
  }
42796
42804
  return {
@@ -42833,9 +42841,12 @@ function runNativeTagsShortcut(input) {
42833
42841
  maxBuffer: 1024 * 1024,
42834
42842
  stdio: ["ignore", "pipe", "pipe"]
42835
42843
  });
42836
- } catch {
42837
- throw new Error(
42838
- `Native tag operation did not complete cleanly; the "${status.shortcut}" Shortcut may be waiting for macOS permission. Do not retry automatically; read the exact note and check that Shortcut in Shortcuts.app`
42844
+ } catch (error2) {
42845
+ throw Object.assign(
42846
+ new Error(
42847
+ `Native tag operation did not complete cleanly. ${shortcutConsentHint(status.shortcut)} Do not retry automatically; read the exact note first`
42848
+ ),
42849
+ { shortcut: status.shortcut, code: error2?.code }
42839
42850
  );
42840
42851
  } finally {
42841
42852
  rmSync2(directory, { recursive: true, force: true });
@@ -42912,6 +42923,8 @@ function comparableVisibleText(html) {
42912
42923
  var BACKGROUND_SHORTCUT = "Apple Notes MCP - Background Operations v5";
42913
42924
  var backgroundStatus = () => nativeTagsStatus(process.env.APPLE_NOTES_MCP_BACKGROUND_SHORTCUT || BACKGROUND_SHORTCUT);
42914
42925
  var nativeTagBridgeStatus = () => nativeTagsStatus();
42926
+ var MARKDOWN_NOTE_SHORTCUT = "Apple Notes MCP - Create Markdown Note";
42927
+ var markdownNoteStatus = () => nativeTagsStatus(process.env.APPLE_NOTES_MCP_MARKDOWN_SHORTCUT || MARKDOWN_NOTE_SHORTCUT);
42915
42928
  function readBackgroundSnapshot(manager, id2) {
42916
42929
  if (!/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i.test(id2))
42917
42930
  throw new Error("Exact note ID required");
@@ -42963,6 +42976,17 @@ var NATIVE_APPEND_ELEMENTS = [
42963
42976
  ];
42964
42977
  var NATIVE_APPEND_SPAN_STYLE = /^font-size\s*:\s*\d{1,3}(?:\.\d+)?(?:px|pt)\s*;?$/i;
42965
42978
  var NATIVE_APPEND_HTML_SUBSET = `Native append accepts ${NATIVE_APPEND_ELEMENTS.map((e) => `<${e}>`).join(" ")}, with href on <a> and a font-size style on <span> as the only attributes; everything else needs update-note.`;
42979
+ var UNDERSCORES_OUTSIDE_A_WORD = "underscores outside a word";
42980
+ var UNMODELED_MARKDOWN = [
42981
+ [/(?<![\p{L}\p{N}])_|_(?![\p{L}\p{N}])/mu, UNDERSCORES_OUTSIDE_A_WORD],
42982
+ [/\\[!-/:-@[-`{-~]/m, "backslash escapes"],
42983
+ [/&(?:#\d+|#x[0-9a-f]+|[a-z][a-z0-9]*);/im, "character references"],
42984
+ [/^ {0,3}(?:=+|-+|(?:\*[ \t]*){3,})[ \t]*$/m, "underline or rule lines"],
42985
+ [/^ {1,3}(?:#|[-+*][ \t]|\d+[.)][ \t])/m, "indented headings or list items"],
42986
+ [/^\d+\)[ \t]/m, "`1)` lists"],
42987
+ [/^#{1,3}[ \t].*[ \t]#+[ \t]*$/m, "closing # sequences"],
42988
+ [/\[[^\]\n]*[*_][^\]\n]*\]\(/m, "formatting inside link labels"]
42989
+ ];
42966
42990
  function validateAppendContent(content, format) {
42967
42991
  if (!content || content.length > 1024 * 1024 || content.includes("\0"))
42968
42992
  throw new Error("Invalid append content (limit 1 MiB)");
@@ -42992,11 +43016,18 @@ function validateAppendContent(content, format) {
42992
43016
  }
42993
43017
  if (/<!--|<!|<\?|<[^>]*$/u.test(content)) throw new Error("Unsupported HTML markup");
42994
43018
  }
42995
- if (format === "markdown" && /!\[|<\/?[a-z]|\]\(\s*(?:javascript|data|file):/i.test(content))
42996
- throw new Error("Markdown images, raw HTML and local/executable links are unsupported");
43019
+ if (format === "markdown") {
43020
+ if (/!\[|<\/?[a-z]|\]\(\s*(?:javascript|data|file):/i.test(content))
43021
+ throw new Error("Markdown images, raw HTML and local/executable links are unsupported");
43022
+ const withoutLinkDestinations = content.replace(/(\[[^\]\n]+\])\([^()\s]+\)/g, "$1()");
43023
+ for (const [pattern, name] of UNMODELED_MARKDOWN)
43024
+ if (pattern.test(name === UNDERSCORES_OUTSIDE_A_WORD ? withoutLinkDestinations : content))
43025
+ throw new Error(
43026
+ `Markdown cannot use ${name}; Notes would change that text, so the result could not be verified`
43027
+ );
43028
+ }
42997
43029
  }
42998
- function runBackgroundShortcut(input) {
42999
- const status = backgroundStatus();
43030
+ function runBackgroundShortcut(input, status = backgroundStatus()) {
43000
43031
  if (!status.installed)
43001
43032
  throw new Error(
43002
43033
  `Install the supplied "${status.shortcut}" Shortcut once; Shortcuts must list it exactly once`
@@ -43098,6 +43129,11 @@ function assertPreserved(before, after, options = {}) {
43098
43129
  }
43099
43130
  }
43100
43131
  }
43132
+ function describeTransportFailure(error2) {
43133
+ const detail = error2;
43134
+ const named = detail?.shortcut ? `the "${detail.shortcut}" Shortcut` : "the background Shortcut";
43135
+ return detail?.code === "ETIMEDOUT" ? `Shortcuts timed out waiting for ${named}. ${shortcutConsentHint(detail.shortcut)}` : `${named} failed: ${String(detail?.stderr || detail?.message || "no output").trim().slice(0, 400)}`;
43136
+ }
43101
43137
  function mutateBackground(request, operation, data, verify, deps) {
43102
43138
  if (request.scopeText.length < 12 || request.scopeText.length > 500 || /[\r\n\0]/u.test(request.scopeText))
43103
43139
  throw new Error("Use a distinctive existing single-line scope of 12\u2013500 characters");
@@ -43117,9 +43153,7 @@ function mutateBackground(request, operation, data, verify, deps) {
43117
43153
  deps.run({ ...data, operation, title: before.title, scopeText: request.scopeText });
43118
43154
  } catch (error2) {
43119
43155
  transportUncertain = true;
43120
- const detail = error2;
43121
- const named = detail?.shortcut ? `the "${detail.shortcut}" Shortcut` : "the background Shortcut";
43122
- transportMessage = detail?.code === "ETIMEDOUT" ? `Shortcuts timed out waiting for ${named}; check for an interactive parameter or permission request` : `${named} failed: ${String(detail?.stderr || detail?.message || "no output").trim().slice(0, 400)}`;
43156
+ transportMessage = describeTransportFailure(error2);
43123
43157
  }
43124
43158
  const after = deps.read(request.id);
43125
43159
  try {
@@ -43207,6 +43241,109 @@ function setNativeTag(manager, request) {
43207
43241
  deps
43208
43242
  );
43209
43243
  }
43244
+ var literalMarkdown = (text) => text.replace(/[!-/:-@[-`{-~]/g, "\\$&");
43245
+ function headingLevels(html) {
43246
+ return [...html.matchAll(/<h([1-3])\b[^>]*>([\s\S]*?)<\/h\1>/gi)].filter((match) => comparableVisibleText(match[2])).map((match) => Number(match[1]));
43247
+ }
43248
+ function createMarkdownNote(manager, request, run = runBackgroundShortcut) {
43249
+ if (/[\r\n\0]/u.test(request.title)) throw new Error("A Markdown note title must be one line");
43250
+ validateAppendContent(request.content, "markdown");
43251
+ const bodyHtml = appendMarkdownHtml(request.content);
43252
+ if (request.folder) buildFolderReference(request.folder);
43253
+ const expectedText = `${request.title} ${comparableVisibleText(bodyHtml)}`.replace(/\s+/gu, " ").trim();
43254
+ const expectedLevels = [1, ...headingLevels(bodyHtml)].join();
43255
+ const status = markdownNoteStatus();
43256
+ if (!status.installed)
43257
+ throw new Error(
43258
+ `Install the supplied "${status.shortcut}" Shortcut once; Shortcuts must list it exactly once`
43259
+ );
43260
+ const segments = (path4) => JSON.stringify(splitFolderPath(path4).map((part) => part.toLocaleLowerCase()));
43261
+ if (request.folder) {
43262
+ const wanted = segments(request.folder);
43263
+ if (!manager.listAccounts().some(
43264
+ (account) => account.defaultFolder && manager.listFolders(account.name).some((folder) => segments(folder.name) === wanted)
43265
+ ))
43266
+ throw new Error(
43267
+ `Folder "${request.folder}" does not exist; create it with create-folder first. Nothing was created`
43268
+ );
43269
+ }
43270
+ const defaultFolderNotes = () => new Map(
43271
+ manager.listAccounts().flatMap(
43272
+ (account) => account.defaultFolder ? manager.listNoteRefs(account.name, account.defaultFolder).map((note) => [note.id, account.name]) : []
43273
+ )
43274
+ );
43275
+ const before = defaultFolderNotes();
43276
+ let transportMessage = "";
43277
+ try {
43278
+ run(
43279
+ {
43280
+ operation: "create-markdown",
43281
+ text: `# ${literalMarkdown(request.title)}
43282
+
43283
+ ${request.content}`
43284
+ },
43285
+ status
43286
+ );
43287
+ } catch (error2) {
43288
+ transportMessage = describeTransportFailure(error2);
43289
+ }
43290
+ const reason = (error2) => error2 instanceof Error ? error2.message : String(error2);
43291
+ let candidates = [];
43292
+ try {
43293
+ const created = [...defaultFolderNotes()].filter(([noteId3]) => !before.has(noteId3));
43294
+ candidates = created.map(([noteId3]) => noteId3);
43295
+ const failures = [];
43296
+ const verified = created.flatMap(([noteId3, account2]) => {
43297
+ try {
43298
+ const note2 = readBackgroundSnapshot(manager, noteId3);
43299
+ if (note2.rich.text.replace(/[\s\ufffc]+/gu, " ").trim() !== expectedText)
43300
+ throw new Error("Note text not verified");
43301
+ if (headingLevels(note2.html).join() !== expectedLevels)
43302
+ throw new Error("Heading styles not verified");
43303
+ assertAppendedHtmlLinks(0, note2.rich.links, bodyHtml);
43304
+ return [{ id: noteId3, account: account2, note: note2 }];
43305
+ } catch (error2) {
43306
+ failures.push(`${noteId3}: ${reason(error2)}`);
43307
+ return [];
43308
+ }
43309
+ });
43310
+ if (verified.length !== 1)
43311
+ throw new Error(
43312
+ verified.length ? `${verified.length} matching notes appeared (${verified.map((v) => v.id).join(", ")})` : created.length ? `no new note verified (${failures.join("; ")})` : "no new note was found in the default folder"
43313
+ );
43314
+ const { id: id2, account } = verified[0];
43315
+ candidates = [id2];
43316
+ let { note } = verified[0];
43317
+ if (request.folder) {
43318
+ const wanted = segments(request.folder);
43319
+ if (!manager.listFolders(account).some((folder) => segments(folder.name) === wanted))
43320
+ throw new Error(
43321
+ `created and verified in the ${account} default folder, but folder "${request.folder}" does not exist in ${account}; create it there, then use move-note with id ${id2} instead of creating the note again`
43322
+ );
43323
+ if (!manager.moveNoteById(id2, request.folder, account))
43324
+ throw new Error(
43325
+ `created and verified in the ${account} default folder, but not moved to "${request.folder}"; use move-note instead of creating it again`
43326
+ );
43327
+ note = readBackgroundSnapshot(manager, id2);
43328
+ }
43329
+ return {
43330
+ ok: true,
43331
+ id: id2,
43332
+ title: request.title,
43333
+ folder: request.folder,
43334
+ account,
43335
+ contentHash: note.hash,
43336
+ verified: true,
43337
+ ...transportMessage ? {
43338
+ transportWarning: "Transport was uncertain; exact-ID readback verified the requested result"
43339
+ } : {}
43340
+ };
43341
+ } catch (error2) {
43342
+ throw new Error(
43343
+ `Operation outcome uncertain; ${candidates.length ? `read ${candidates.length === 1 ? "note" : "notes"} ${candidates.join(", ")}` : "search for the title"} before any retry: ${reason(error2)}${transportMessage ? `; ${transportMessage}` : ""}`
43344
+ );
43345
+ }
43346
+ }
43210
43347
 
43211
43348
  // src/tools/doctor.ts
43212
43349
  function runDoctor(manager) {
@@ -43239,15 +43376,17 @@ function runDoctor(manager) {
43239
43376
  status: fda ? "ok" : "warn",
43240
43377
  detail: fda ? "granted \u2014 the Notes database is readable (checklist state, note metadata, note links, sync detail)" : `not granted \u2014 get-checklist-state, get-note-metadata, and the checklist annotations in get-note-markdown won't work; get-note-link fails on macOS 26+ (macOS 12-15 falls back to AppleScript); get-sync-status still answers but cannot see pending uploads. Everything else is pure AppleScript and is unaffected. In System Settings > Privacy & Security > Full Disk Access, grant access to the app that launches this server (Claude Desktop / Terminal / iTerm2), then fully quit and relaunch it and re-run doctor. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL}`
43241
43378
  });
43379
+ const consentReminder = "After install or upgrade, run each bridge Shortcut once in the foreground in Shortcuts.app and choose Always Allow: a background run cannot display a first-run consent prompt, so an unanswered one stalls that bridge's native writes until they time out while this check stays ok";
43242
43380
  try {
43243
43381
  const bridgeStatuses = [NATIVE_TAGS_SHORTCUT, BACKGROUND_SHORTCUT].map(
43244
43382
  (name) => nativeTagsStatus(name)
43245
43383
  );
43246
43384
  const missing = bridgeStatuses.filter((status) => !status.installed);
43385
+ const markdown = markdownBridgeDetail();
43247
43386
  checks.push({
43248
43387
  name: "Native write Shortcuts",
43249
43388
  status: missing.length ? "warn" : "ok",
43250
- detail: missing.length ? `missing: ${missing.map((status) => status.shortcut).join(", ")}. Run apple-notes-mcp setup and approve Add Shortcut in macOS` : "both native-write bridges are installed"
43389
+ detail: missing.length ? `missing: ${missing.map((status) => status.shortcut).join(", ")}. Run apple-notes-mcp setup and approve Add Shortcut in macOS. ${markdown} ${consentReminder}` : `both native-write bridges are installed. ${markdown} ${consentReminder}`
43251
43390
  });
43252
43391
  } catch (error2) {
43253
43392
  checks.push({
@@ -43260,6 +43399,14 @@ function runDoctor(manager) {
43260
43399
  const healthy = !checks.some((c) => c.status === "fail");
43261
43400
  return { healthy, checks };
43262
43401
  }
43402
+ function markdownBridgeDetail() {
43403
+ const purpose = 'needed only for create-note format: "markdown" on macOS 26+';
43404
+ try {
43405
+ return nativeTagsStatus(MARKDOWN_NOTE_SHORTCUT).installed ? `Optional ${MARKDOWN_NOTE_SHORTCUT} bridge: installed (${purpose}).` : `Optional ${MARKDOWN_NOTE_SHORTCUT} bridge: not installed (${purpose}; apple-notes-mcp setup offers it).`;
43406
+ } catch (error2) {
43407
+ return `Optional ${MARKDOWN_NOTE_SHORTCUT} bridge: could not inspect (${purpose}): ${String(error2)}.`;
43408
+ }
43409
+ }
43263
43410
  function checkNodeRuntimeSignature() {
43264
43411
  const name = "Node runtime signature";
43265
43412
  try {
@@ -43882,7 +44029,8 @@ var VERIFIED_BACKGROUND = /* @__PURE__ */ new Set([
43882
44029
  "insert-note-link",
43883
44030
  "set-note-pinned",
43884
44031
  "remove-native-tags",
43885
- "replace-native-tag"
44032
+ "replace-native-tag",
44033
+ "create-note-markdown"
43886
44034
  ]);
43887
44035
  var LIVE_VALIDATION_BLOCKERS = {};
43888
44036
  var signingRefusal = "Installed Shortcuts refuses to sign this Notes action (unsupported features); no background fallback is enabled";
@@ -43970,16 +44118,24 @@ function registerNativeOperations(server2, manager) {
43970
44118
  "set-note-pinned",
43971
44119
  "remove-native-tags",
43972
44120
  "replace-native-tag",
43973
- "insert-note-link"
44121
+ "insert-note-link",
44122
+ "create-note-markdown"
43974
44123
  ];
43975
44124
  let tagBridgeInstalled = false;
43976
44125
  try {
43977
44126
  tagBridgeInstalled = nativeTagBridgeStatus().installed;
43978
44127
  } catch {
43979
44128
  }
44129
+ let markdownBridgeInstalled = false;
44130
+ try {
44131
+ markdownBridgeInstalled = markdownNoteStatus().installed;
44132
+ } catch {
44133
+ }
44134
+ const installed = (name) => name === "create-note-markdown" ? markdownBridgeInstalled : bridge.installed;
43980
44135
  return {
43981
44136
  bridge,
43982
44137
  nativeTagBridgeInstalled: tagBridgeInstalled,
44138
+ markdownNoteBridgeInstalled: markdownBridgeInstalled,
43983
44139
  mode: "background-only",
43984
44140
  operations: Object.fromEntries(
43985
44141
  native.map((name) => [
@@ -43987,8 +44143,8 @@ function registerNativeOperations(server2, manager) {
43987
44143
  {
43988
44144
  implemented: true,
43989
44145
  verified: VERIFIED_BACKGROUND.has(name),
43990
- available: VERIFIED_BACKGROUND.has(name) && (!native.includes(name) || bridge.installed) && (name !== "replace-native-tag" || tagBridgeInstalled),
43991
- reason: !VERIFIED_BACKGROUND.has(name) ? UNAVAILABLE[name] || LIVE_VALIDATION_BLOCKERS[name] || "Live validation pending; install the shortcut and complete the isolated acceptance tests" : native.includes(name) && !bridge.installed ? "Run apple-notes-mcp setup and approve Add Shortcut in macOS" : name === "replace-native-tag" && !tagBridgeInstalled ? "Run apple-notes-mcp setup to install the Native Tags bridge for the addition phase" : void 0
44146
+ available: VERIFIED_BACKGROUND.has(name) && installed(name) && (name !== "replace-native-tag" || tagBridgeInstalled),
44147
+ reason: !VERIFIED_BACKGROUND.has(name) ? UNAVAILABLE[name] || LIVE_VALIDATION_BLOCKERS[name] || "Live validation pending; install the shortcut and complete the isolated acceptance tests" : !installed(name) ? "Run apple-notes-mcp setup and approve Add Shortcut in macOS" : name === "replace-native-tag" && !tagBridgeInstalled ? "Run apple-notes-mcp setup to install the Native Tags bridge for the addition phase" : void 0
43992
44148
  }
43993
44149
  ])
43994
44150
  ),
@@ -44205,13 +44361,21 @@ function registerNativeOperations(server2, manager) {
44205
44361
  // src/setupShortcuts.ts
44206
44362
  import { spawnSync as spawnSync2 } from "node:child_process";
44207
44363
  import { existsSync as existsSync7 } from "node:fs";
44364
+ import { release } from "node:os";
44208
44365
  import { dirname as dirname2, resolve as resolve2 } from "node:path";
44209
44366
  import { fileURLToPath } from "node:url";
44367
+ var OPTIONAL_BRIDGE_NOTE = "(optional \u2014 needed only for create-note format: markdown, macOS 26+)";
44368
+ var MARKDOWN_MIN_DARWIN_MAJOR = 25;
44210
44369
  var shortcutFiles = [
44211
44370
  { name: NATIVE_TAGS_SHORTCUT, file: "Apple Notes MCP - Native Tags.shortcut" },
44212
44371
  {
44213
44372
  name: BACKGROUND_SHORTCUT,
44214
44373
  file: "Apple Notes MCP - Background Operations v5.shortcut"
44374
+ },
44375
+ {
44376
+ name: MARKDOWN_NOTE_SHORTCUT,
44377
+ file: "Apple Notes MCP - Create Markdown Note.shortcut",
44378
+ optional: true
44215
44379
  }
44216
44380
  ];
44217
44381
  function setupShortcuts(checkOnly, dependencies = {}) {
@@ -44222,7 +44386,9 @@ function setupShortcuts(checkOnly, dependencies = {}) {
44222
44386
  return result.status === 0 ? { ok: true } : { ok: false, error: result.stderr || result.error?.message || "open failed" };
44223
44387
  });
44224
44388
  const baseDirectory = dependencies.baseDirectory || resolve2(dirname2(fileURLToPath(import.meta.url)), "../shortcuts");
44225
- const items = shortcutFiles.map(({ name, file }) => {
44389
+ const osRelease = (dependencies.osRelease || release)();
44390
+ const darwinMajor = Number.parseInt(osRelease.split(".")[0], 10);
44391
+ const items = shortcutFiles.map(({ name, file, optional: optional2 }) => {
44226
44392
  const path4 = resolve2(baseDirectory, file);
44227
44393
  let installed = false;
44228
44394
  let identifier;
@@ -44235,32 +44401,49 @@ function setupShortcuts(checkOnly, dependencies = {}) {
44235
44401
  error2 = cause instanceof Error ? cause.message : String(cause);
44236
44402
  }
44237
44403
  let opened = false;
44404
+ let skipped;
44238
44405
  if (!installed && !checkOnly) {
44239
- if (!exists(path4)) error2 = `Packaged Shortcut is missing: ${path4}`;
44406
+ if (optional2 && !(darwinMajor >= MARKDOWN_MIN_DARWIN_MAJOR))
44407
+ skipped = `requires macOS 26 or later (this Mac reports Darwin ${osRelease})`;
44408
+ else if (!exists(path4)) error2 = `Packaged Shortcut is missing: ${path4}`;
44240
44409
  else {
44241
44410
  const result = open(path4);
44242
44411
  opened = result.ok;
44243
44412
  if (!result.ok) error2 = result.error || `Could not open ${file}`;
44244
44413
  }
44245
44414
  }
44246
- return { name, installed, identifier, file: path4, opened, ...error2 ? { error: error2 } : {} };
44415
+ return {
44416
+ name,
44417
+ installed,
44418
+ identifier,
44419
+ file: path4,
44420
+ opened,
44421
+ ...optional2 ? { optional: optional2 } : {},
44422
+ ...skipped ? { skipped } : {},
44423
+ ...error2 ? { error: error2 } : {}
44424
+ };
44247
44425
  });
44248
- return { ready: items.every((item) => item.installed), checkOnly, items };
44426
+ return { ready: items.every((item) => item.optional || item.installed), checkOnly, items };
44249
44427
  }
44250
44428
  function formatShortcutSetup(report) {
44251
44429
  const lines = ["Apple Notes MCP Shortcut setup", ""];
44252
44430
  for (const item of report.items) {
44253
- if (item.installed) lines.push(`\u2713 ${item.name} (${item.identifier})`);
44254
- else if (item.opened) lines.push(`\u2192 ${item.name}: confirm \u201CAdd Shortcut\u201D in macOS`);
44255
- else lines.push(`\u2717 ${item.name}: ${item.error || "not installed"}`);
44431
+ const note = item.optional ? ` ${OPTIONAL_BRIDGE_NOTE}` : "";
44432
+ if (item.installed) lines.push(`\u2713 ${item.name} (${item.identifier})${note}`);
44433
+ else if (item.opened) lines.push(`\u2192 ${item.name}: confirm \u201CAdd Shortcut\u201D in macOS${note}`);
44434
+ else if (item.skipped) lines.push(`\u2013 ${item.name}: skipped, ${item.skipped}${note}`);
44435
+ else lines.push(`\u2717 ${item.name}: ${item.error || "not installed"}${note}`);
44256
44436
  }
44257
44437
  lines.push("");
44258
- if (report.ready) lines.push("Both Shortcut bridges are installed.");
44438
+ if (report.ready) lines.push("Both required Shortcut bridges are installed.");
44259
44439
  else if (report.checkOnly) lines.push("Run `apple-notes-mcp setup` to open missing workflows.");
44260
44440
  else
44261
44441
  lines.push(
44262
44442
  "After approving the macOS dialogs, run `apple-notes-mcp setup --check` or the MCP doctor tool."
44263
44443
  );
44444
+ lines.push(
44445
+ "After install or upgrade, run each bridge once in the foreground in Shortcuts.app and choose Always Allow; a background run cannot display a first-run consent prompt and stalls until it times out."
44446
+ );
44264
44447
  return lines.join("\n");
44265
44448
  }
44266
44449
 
@@ -44368,9 +44551,11 @@ registerTool(
44368
44551
  content: external_exports.string().min(1, "Content is required").max(MAX.CONTENT).describe(
44369
44552
  'Note body. AppleScript cannot create true Apple Notes checklists \u2014 `<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 \u21E7\u2318L.'
44370
44553
  ),
44371
- format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
44554
+ format: external_exports.enum(["plaintext", "html", "markdown"]).optional().default("plaintext").describe(
44555
+ "Content format: 'plaintext' (default), 'html' for rich formatting, or 'markdown' for real Title/Heading/Subheading styles through the Create Markdown Note Shortcut (iCloud only; see get-capabilities)"
44556
+ ),
44372
44557
  tags: external_exports.array(external_exports.string().max(MAX.TAG)).max(MAX.TAGS).optional().describe(
44373
- "Returned-only metadata \u2014 NOT written to Notes.app. Apple Notes tags can't be set via AppleScript, so any values passed here are echoed back in the response but do not appear on the created note. Use #hashtags in the body for searchable text; this does not create native tag objects. Native tags need the Notes Shortcuts action."
44558
+ "Returned-only metadata \u2014 NOT written to Notes.app. Apple Notes tags can't be set via AppleScript, so any values passed here are echoed back in the response but do not appear on the created note. Use #hashtags in the body for searchable text; this does not create native tag objects. Native tags need the Notes Shortcuts action. Refused with format 'markdown': add tags afterwards with add-native-tags."
44374
44559
  ),
44375
44560
  folder: external_exports.string().max(MAX.FOLDER).optional().describe(
44376
44561
  "Folder to create the note in (supports nested paths like 'Work/Clients'). The folder must already exist \u2014 this tool does not create it; call create-folder first, which is idempotent and creates intermediate segments."
@@ -44390,6 +44575,19 @@ registerTool(
44390
44575
  }
44391
44576
  },
44392
44577
  withErrorHandling(({ title, content, format = "plaintext", tags = [], folder, account }) => {
44578
+ if (format === "markdown") {
44579
+ if (account)
44580
+ return errorResponse(
44581
+ "Markdown notes are created in the iCloud account, the only one where Notes interprets Markdown; omit account"
44582
+ );
44583
+ if (tags.length)
44584
+ return errorResponse(
44585
+ 'tags are not supported with format "markdown"; create the note without tags, then add them with add-native-tags using the returned id'
44586
+ );
44587
+ requireValidated("create-note-markdown");
44588
+ const result = createMarkdownNote(notesManager, { title, content, folder });
44589
+ return successResponse(`Note created from Markdown: "${title}" [id: ${result.id}]`, result);
44590
+ }
44393
44591
  const note = notesManager.createNote(title, content, tags, folder, account, format);
44394
44592
  if (!note) {
44395
44593
  const target = folder ? ` Most often the folder "${folder}" does not exist: run list-folders to check, then create-folder to create it (it is idempotent and creates intermediate segments).` : account ? ` Most often the account "${account}" is not configured in Notes.app: run list-accounts to check the exact name.` : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.8.14",
3
+ "version": "2.8.16",
4
4
  "description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -0,0 +1,394 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>WFWorkflowName</key>
6
+ <string>Apple Notes MCP - Create Markdown Note</string>
7
+ <key>WFWorkflowActions</key>
8
+ <array>
9
+ <dict>
10
+ <key>WFWorkflowActionIdentifier</key>
11
+ <string>is.workflow.actions.comment</string>
12
+ <key>WFWorkflowActionParameters</key>
13
+ <dict>
14
+ <key>UUID</key>
15
+ <string>9677FA88-6915-5FA3-A68B-56A5E5257DE6</string>
16
+ <key>WFCommentActionText</key>
17
+ <string>Creates one note from Markdown with Notes' own importer, in the iCloud default folder. Input: JSON {operation: create-markdown, text}. No search, edits, deletion, shell scripts, network or arbitrary action execution. The MCP finds the new note among notes added to the default folder and verifies it by exact-ID readback.</string>
18
+ </dict>
19
+ </dict>
20
+ <dict>
21
+ <key>WFWorkflowActionIdentifier</key>
22
+ <string>is.workflow.actions.detect.dictionary</string>
23
+ <key>WFWorkflowActionParameters</key>
24
+ <dict>
25
+ <key>UUID</key>
26
+ <string>C826AE0C-31B7-508F-8AA6-2C1627425D3B</string>
27
+ <key>WFInput</key>
28
+ <dict>
29
+ <key>Value</key>
30
+ <dict>
31
+ <key>Type</key>
32
+ <string>ExtensionInput</string>
33
+ </dict>
34
+ <key>WFSerializationType</key>
35
+ <string>WFTextTokenAttachment</string>
36
+ </dict>
37
+ </dict>
38
+ </dict>
39
+ <dict>
40
+ <key>WFWorkflowActionIdentifier</key>
41
+ <string>is.workflow.actions.getvalueforkey</string>
42
+ <key>WFWorkflowActionParameters</key>
43
+ <dict>
44
+ <key>UUID</key>
45
+ <string>D8DFFD6F-2EF2-5F63-8B77-9578929A96CD</string>
46
+ <key>WFInput</key>
47
+ <dict>
48
+ <key>Value</key>
49
+ <dict>
50
+ <key>Type</key>
51
+ <string>ActionOutput</string>
52
+ <key>OutputUUID</key>
53
+ <string>C826AE0C-31B7-508F-8AA6-2C1627425D3B</string>
54
+ <key>OutputName</key>
55
+ <string>request</string>
56
+ </dict>
57
+ <key>WFSerializationType</key>
58
+ <string>WFTextTokenAttachment</string>
59
+ </dict>
60
+ <key>WFDictionaryKey</key>
61
+ <string>operation</string>
62
+ <key>WFGetDictionaryValueType</key>
63
+ <string>Value</string>
64
+ </dict>
65
+ </dict>
66
+ <dict>
67
+ <key>WFWorkflowActionIdentifier</key>
68
+ <string>is.workflow.actions.gettext</string>
69
+ <key>WFWorkflowActionParameters</key>
70
+ <dict>
71
+ <key>UUID</key>
72
+ <string>BA3704CC-7103-5406-A7A9-4ACA96514A95</string>
73
+ <key>WFTextActionText</key>
74
+ <dict>
75
+ <key>WFSerializationType</key>
76
+ <string>WFTextTokenString</string>
77
+ <key>Value</key>
78
+ <dict>
79
+ <key>string</key>
80
+ <string></string>
81
+ <key>attachmentsByRange</key>
82
+ <dict>
83
+ <key>{0, 1}</key>
84
+ <dict>
85
+ <key>Type</key>
86
+ <string>ActionOutput</string>
87
+ <key>OutputUUID</key>
88
+ <string>D8DFFD6F-2EF2-5F63-8B77-9578929A96CD</string>
89
+ <key>OutputName</key>
90
+ <string>operation-value</string>
91
+ </dict>
92
+ </dict>
93
+ </dict>
94
+ </dict>
95
+ </dict>
96
+ </dict>
97
+ <dict>
98
+ <key>WFWorkflowActionIdentifier</key>
99
+ <string>is.workflow.actions.getvalueforkey</string>
100
+ <key>WFWorkflowActionParameters</key>
101
+ <dict>
102
+ <key>UUID</key>
103
+ <string>9EC8FFF3-7821-592E-86A4-BC712A669591</string>
104
+ <key>WFInput</key>
105
+ <dict>
106
+ <key>Value</key>
107
+ <dict>
108
+ <key>Type</key>
109
+ <string>ActionOutput</string>
110
+ <key>OutputUUID</key>
111
+ <string>C826AE0C-31B7-508F-8AA6-2C1627425D3B</string>
112
+ <key>OutputName</key>
113
+ <string>request</string>
114
+ </dict>
115
+ <key>WFSerializationType</key>
116
+ <string>WFTextTokenAttachment</string>
117
+ </dict>
118
+ <key>WFDictionaryKey</key>
119
+ <string>text</string>
120
+ <key>WFGetDictionaryValueType</key>
121
+ <string>Value</string>
122
+ </dict>
123
+ </dict>
124
+ <dict>
125
+ <key>WFWorkflowActionIdentifier</key>
126
+ <string>is.workflow.actions.gettext</string>
127
+ <key>WFWorkflowActionParameters</key>
128
+ <dict>
129
+ <key>UUID</key>
130
+ <string>68CEF93A-EBC3-5F0C-831D-B459A0AEA703</string>
131
+ <key>WFTextActionText</key>
132
+ <dict>
133
+ <key>WFSerializationType</key>
134
+ <string>WFTextTokenString</string>
135
+ <key>Value</key>
136
+ <dict>
137
+ <key>string</key>
138
+ <string></string>
139
+ <key>attachmentsByRange</key>
140
+ <dict>
141
+ <key>{0, 1}</key>
142
+ <dict>
143
+ <key>Type</key>
144
+ <string>ActionOutput</string>
145
+ <key>OutputUUID</key>
146
+ <string>9EC8FFF3-7821-592E-86A4-BC712A669591</string>
147
+ <key>OutputName</key>
148
+ <string>text-value</string>
149
+ </dict>
150
+ </dict>
151
+ </dict>
152
+ </dict>
153
+ </dict>
154
+ </dict>
155
+ <dict>
156
+ <key>WFWorkflowActionIdentifier</key>
157
+ <string>is.workflow.actions.conditional</string>
158
+ <key>WFWorkflowActionParameters</key>
159
+ <dict>
160
+ <key>UUID</key>
161
+ <string>C49370C1-C96A-567A-91A8-75AB9A8E9695</string>
162
+ <key>GroupingIdentifier</key>
163
+ <string>0DD3CC74-CF2E-5A9D-AE11-C1DC205557E8</string>
164
+ <key>WFControlFlowMode</key>
165
+ <integer>0</integer>
166
+ <key>WFInput</key>
167
+ <dict>
168
+ <key>Type</key>
169
+ <string>Variable</string>
170
+ <key>Variable</key>
171
+ <dict>
172
+ <key>Value</key>
173
+ <dict>
174
+ <key>Type</key>
175
+ <string>ActionOutput</string>
176
+ <key>OutputUUID</key>
177
+ <string>BA3704CC-7103-5406-A7A9-4ACA96514A95</string>
178
+ <key>OutputName</key>
179
+ <string>operation</string>
180
+ </dict>
181
+ <key>WFSerializationType</key>
182
+ <string>WFTextTokenAttachment</string>
183
+ </dict>
184
+ </dict>
185
+ <key>WFCondition</key>
186
+ <integer>4</integer>
187
+ <key>WFConditionalActionString</key>
188
+ <string>create-markdown</string>
189
+ </dict>
190
+ </dict>
191
+ <dict>
192
+ <key>WFWorkflowActionIdentifier</key>
193
+ <string>com.apple.mobilenotes.SharingExtension</string>
194
+ <key>WFWorkflowActionParameters</key>
195
+ <dict>
196
+ <key>UUID</key>
197
+ <string>ACE3922A-BF8C-5266-88EF-5492F1E16DD9</string>
198
+ <key>WFCreateNoteInput</key>
199
+ <dict>
200
+ <key>WFSerializationType</key>
201
+ <string>WFTextTokenString</string>
202
+ <key>Value</key>
203
+ <dict>
204
+ <key>string</key>
205
+ <string></string>
206
+ <key>attachmentsByRange</key>
207
+ <dict>
208
+ <key>{0, 1}</key>
209
+ <dict>
210
+ <key>Type</key>
211
+ <string>ActionOutput</string>
212
+ <key>OutputUUID</key>
213
+ <string>68CEF93A-EBC3-5F0C-831D-B459A0AEA703</string>
214
+ <key>OutputName</key>
215
+ <string>text</string>
216
+ </dict>
217
+ </dict>
218
+ </dict>
219
+ </dict>
220
+ <key>interpretAsMarkdown</key>
221
+ <true/>
222
+ <key>OpenWhenRun</key>
223
+ <false/>
224
+ <key>WFNoteGroup</key>
225
+ <dict>
226
+ <key>DisplayString</key>
227
+ <string>Notes</string>
228
+ <key>Identifier</key>
229
+ <string>applenotes:folder/DefaultFolder-CloudKit</string>
230
+ </dict>
231
+ <key>folder</key>
232
+ <dict>
233
+ <key>identifier</key>
234
+ <string>applenotes:folder/DefaultFolder-CloudKit</string>
235
+ <key>subtitle</key>
236
+ <dict>
237
+ <key>key</key>
238
+ <string>Notes</string>
239
+ </dict>
240
+ <key>symbol</key>
241
+ <dict>
242
+ <key>systemName</key>
243
+ <string>folder</string>
244
+ </dict>
245
+ <key>title</key>
246
+ <dict>
247
+ <key>key</key>
248
+ <string>Notes</string>
249
+ </dict>
250
+ </dict>
251
+ <key>AppIntentDescriptor</key>
252
+ <dict>
253
+ <key>AppIntentIdentifier</key>
254
+ <string>CreateNoteLinkAction</string>
255
+ <key>BundleIdentifier</key>
256
+ <string>com.apple.Notes</string>
257
+ <key>TeamIdentifier</key>
258
+ <string>0000000000</string>
259
+ <key>Name</key>
260
+ <string>Notes</string>
261
+ </dict>
262
+ </dict>
263
+ </dict>
264
+ <dict>
265
+ <key>WFWorkflowActionIdentifier</key>
266
+ <string>is.workflow.actions.gettext</string>
267
+ <key>WFWorkflowActionParameters</key>
268
+ <dict>
269
+ <key>UUID</key>
270
+ <string>01BCDBDD-C08D-5147-9FA4-EAA5AEE0249C</string>
271
+ <key>WFTextActionText</key>
272
+ <string>APPLE_NOTES_MARKDOWN_V1_DONE</string>
273
+ </dict>
274
+ </dict>
275
+ <dict>
276
+ <key>WFWorkflowActionIdentifier</key>
277
+ <string>is.workflow.actions.output</string>
278
+ <key>WFWorkflowActionParameters</key>
279
+ <dict>
280
+ <key>UUID</key>
281
+ <string>3E99E727-244A-5162-808E-627E927B0BA7</string>
282
+ <key>WFOutput</key>
283
+ <dict>
284
+ <key>WFSerializationType</key>
285
+ <string>WFTextTokenString</string>
286
+ <key>Value</key>
287
+ <dict>
288
+ <key>string</key>
289
+ <string></string>
290
+ <key>attachmentsByRange</key>
291
+ <dict>
292
+ <key>{0, 1}</key>
293
+ <dict>
294
+ <key>Type</key>
295
+ <string>ActionOutput</string>
296
+ <key>OutputUUID</key>
297
+ <string>01BCDBDD-C08D-5147-9FA4-EAA5AEE0249C</string>
298
+ <key>OutputName</key>
299
+ <string>op-create-markdown-ok</string>
300
+ </dict>
301
+ </dict>
302
+ </dict>
303
+ </dict>
304
+ </dict>
305
+ </dict>
306
+ <dict>
307
+ <key>WFWorkflowActionIdentifier</key>
308
+ <string>is.workflow.actions.conditional</string>
309
+ <key>WFWorkflowActionParameters</key>
310
+ <dict>
311
+ <key>UUID</key>
312
+ <string>64FA875D-F93B-5A6E-91E2-888D6F3192AF</string>
313
+ <key>GroupingIdentifier</key>
314
+ <string>0DD3CC74-CF2E-5A9D-AE11-C1DC205557E8</string>
315
+ <key>WFControlFlowMode</key>
316
+ <integer>2</integer>
317
+ </dict>
318
+ </dict>
319
+ <dict>
320
+ <key>WFWorkflowActionIdentifier</key>
321
+ <string>is.workflow.actions.gettext</string>
322
+ <key>WFWorkflowActionParameters</key>
323
+ <dict>
324
+ <key>UUID</key>
325
+ <string>E3837313-32E2-513F-9F22-48146FAB7FCC</string>
326
+ <key>WFTextActionText</key>
327
+ <string>APPLE_NOTES_MARKDOWN_V1_REFUSED</string>
328
+ </dict>
329
+ </dict>
330
+ <dict>
331
+ <key>WFWorkflowActionIdentifier</key>
332
+ <string>is.workflow.actions.output</string>
333
+ <key>WFWorkflowActionParameters</key>
334
+ <dict>
335
+ <key>UUID</key>
336
+ <string>461330AD-B74B-54C8-BE23-04649AA3DCE8</string>
337
+ <key>WFOutput</key>
338
+ <dict>
339
+ <key>WFSerializationType</key>
340
+ <string>WFTextTokenString</string>
341
+ <key>Value</key>
342
+ <dict>
343
+ <key>string</key>
344
+ <string></string>
345
+ <key>attachmentsByRange</key>
346
+ <dict>
347
+ <key>{0, 1}</key>
348
+ <dict>
349
+ <key>Type</key>
350
+ <string>ActionOutput</string>
351
+ <key>OutputUUID</key>
352
+ <string>E3837313-32E2-513F-9F22-48146FAB7FCC</string>
353
+ <key>OutputName</key>
354
+ <string>refused</string>
355
+ </dict>
356
+ </dict>
357
+ </dict>
358
+ </dict>
359
+ </dict>
360
+ </dict>
361
+ </array>
362
+ <key>WFWorkflowClientVersion</key>
363
+ <string>2600</string>
364
+ <key>WFWorkflowMinimumClientVersion</key>
365
+ <integer>900</integer>
366
+ <key>WFWorkflowMinimumClientVersionString</key>
367
+ <string>900</string>
368
+ <key>WFWorkflowTypes</key>
369
+ <array/>
370
+ <key>WFWorkflowHasShortcutInputVariables</key>
371
+ <true/>
372
+ <key>WFWorkflowHasOutputAction</key>
373
+ <true/>
374
+ <key>WFWorkflowInputContentItemClasses</key>
375
+ <array>
376
+ <string>WFStringContentItem</string>
377
+ <string>WFDictionaryContentItem</string>
378
+ <string>WFGenericFileContentItem</string>
379
+ </array>
380
+ <key>WFWorkflowOutputContentItemClasses</key>
381
+ <array>
382
+ <string>WFStringContentItem</string>
383
+ </array>
384
+ <key>WFWorkflowIcon</key>
385
+ <dict>
386
+ <key>WFWorkflowIconStartColor</key>
387
+ <integer>4251333119</integer>
388
+ <key>WFWorkflowIconGlyphNumber</key>
389
+ <integer>59507</integer>
390
+ </dict>
391
+ <key>WFWorkflowImportQuestions</key>
392
+ <array/>
393
+ </dict>
394
+ </plist>
@@ -15,6 +15,13 @@ Open the signed file and confirm **Add Shortcut** once. macOS may also request
15
15
  Notes access on first execution. The `shortcuts` CLI can run an installed
16
16
  workflow but cannot silently import one.
17
17
 
18
+ After install and after every upgrade, run each bridge once in the foreground in
19
+ Shortcuts.app and choose **Always Allow** when it asks for permission. The
20
+ server runs bridges in the background, where Shortcuts cannot display a
21
+ first-run consent prompt, so an unanswered one stalls that bridge's native
22
+ writes until they time out. Quitting or relaunching Shortcuts.app or Notes.app
23
+ does not clear it; one foreground run per bridge does.
24
+
18
25
  The bridge receives a private temporary JSON file with a title, a distinctive
19
26
  existing scope phrase, and normalized tags. Both the server and workflow refuse
20
27
  ambiguous note selection. Server readback, rather than CLI output, establishes
@@ -38,6 +45,39 @@ shortcuts sign --mode anyone \
38
45
  --output "shortcuts/Apple Notes MCP - Background Operations v5.shortcut"
39
46
  ```
40
47
 
41
- Open the signed file and confirm **Add Shortcut** once. Do not install obsolete
48
+ Open the signed file and confirm **Add Shortcut** once, then run it once in the
49
+ foreground in Shortcuts.app and choose **Always Allow** (see above). Do not install obsolete
42
50
  workflow versions with the same display name; the server resolves one exact
43
51
  installed UUID and refuses duplicates.
52
+
53
+ ## Create Markdown Note bridge
54
+
55
+ `Apple Notes MCP - Create Markdown Note.shortcut` creates one note with Notes'
56
+ Create Note action and "Interpret as Markdown" (macOS 26+, iCloud accounts only)
57
+ in the iCloud default folder. It is optional: only `create-note` with
58
+ `format: "markdown"` uses it, and neither `setup --check`'s readiness nor
59
+ `doctor`'s native-write check depends on it. It reads the same JSON request file and runs only
60
+ for `operation: "create-markdown"`; it has no search, edit or delete actions.
61
+ The server finds the new note by exact ID and verifies it before reporting
62
+ success.
63
+
64
+ Regenerate and test the unsigned source with:
65
+
66
+ ```sh
67
+ PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-markdown-note-shortcut.py
68
+ PYTHONDONTWRITEBYTECODE=1 python3 scripts/build-markdown-note-shortcut.py
69
+ shortcuts sign --mode anyone \
70
+ --input "shortcuts/Apple Notes MCP - Create Markdown Note.unsigned.shortcut" \
71
+ --output "shortcuts/Apple Notes MCP - Create Markdown Note.shortcut"
72
+ ```
73
+
74
+ Open the signed file and confirm **Add Shortcut** once. Then run it once in the
75
+ foreground so macOS can ask for Notes access, and choose **Always Allow**. A
76
+ background run cannot show that prompt and waits until the server times out.
77
+ Run by hand with no input, the Shortcut stops before reaching Notes, so start the
78
+ first run with a request instead. This creates a note titled "Apple Notes MCP
79
+ check", which you can delete:
80
+
81
+ ```sh
82
+ open "shortcuts://run-shortcut?name=Apple%20Notes%20MCP%20-%20Create%20Markdown%20Note&input=text&text=%7B%22operation%22%3A%22create-markdown%22%2C%22text%22%3A%22%23%20Apple%20Notes%20MCP%20check%22%7D"
83
+ ```