apple-notes-mcp 2.8.15 → 2.8.17

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,22 +59,30 @@ 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
 
74
79
  After install **and after every upgrade**, open Shortcuts.app and run each
75
- bridge — `Apple Notes MCP - Native Tags` and
76
- `Apple Notes MCP - Background Operations v5` — once in the foreground, choosing
77
- **Always Allow** when Shortcuts asks for permission. The server runs these
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
78
86
  Shortcuts in the background, where Shortcuts cannot display a first-run consent
79
87
  prompt: an unanswered one stalls every native write on that bridge until it
80
88
  times out, while `doctor` still reports the bridge installed. Quitting or
@@ -202,10 +210,10 @@ Creates a new note in Apple Notes.
202
210
  |-----------|------|----------|-------------|
203
211
  | `title` | string | Yes | The title of the note. Automatically prepended as `<h1>` — do NOT include the title in `content` |
204
212
  | `content` | string | Yes | The body content of the note (do not repeat the title here) |
205
- | `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"` |
206
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 |
207
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) |
208
- | `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) |
209
217
 
210
218
  **Example (tagged with inline hashtags):**
211
219
  ```json
@@ -235,13 +243,63 @@ Creates a new note in Apple Notes.
235
243
 
236
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.
237
245
 
238
- > **Known limitation:** `create-note` sets the note body directly via AppleScript's
239
- > `body` property, which does not apply real Notes paragraph styles for interior
240
- > content — an `<h2>`/`<h3>` tag or a `<span style="font-size: …px">` heading span
241
- > in `content` renders as plain bold, styled text, not an actual Heading or
242
- > Subheading ([#172](https://github.com/sweetrb/apple-notes-mcp/issues/172)). Real
243
- > Heading/Subheading styles can currently only be added to a note that already
244
- > 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).
245
303
 
246
304
  **Returns:** Confirmation message with note title and ID. Save the ID for subsequent operations like `update-note`, `delete-note`, etc.
247
305
 
@@ -1073,9 +1131,11 @@ Install the signed workflows once as described in
1073
1131
  foreground in Shortcuts.app and choose **Always Allow** — again after every
1074
1132
  upgrade — so no first-run consent prompt is left for a background run that
1075
1133
  cannot display it. Native writes never use UI
1076
- automation or write directly to the Notes database. Every operation requires an
1077
- exact note ID, a fresh `expectedContentHash`, and a distinctive existing
1078
- `scopeText` so the Shortcut and server can independently resolve the same note.
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"`.
1079
1139
 
1080
1140
  #### `get-capabilities`
1081
1141
 
@@ -1095,7 +1155,10 @@ subset as `format: "html"`'s Markdown-shaped content (`#`/`##`/`###` headings,
1095
1155
  flat lists, emphasis, and inline links) but is sent through Notes' own native
1096
1156
  Markdown importer rather than converted to HTML first, so `#`/`##`/`###`
1097
1157
  produce real Title/Heading/Subheading — Notes' HTML importer only
1098
- 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
1099
1162
  transport failure names the Shortcut it was waiting on, so a missing or
1100
1163
  unapproved bridge is identified rather than guessed at; a timeout also says it
1101
1164
  may be an unanswered first-run consent prompt and names the Shortcut to run once
@@ -1417,7 +1480,7 @@ In a JSON string literal the two characters `\\` denote **one** literal backslas
1417
1480
  ### Native writes time out or report an uncertain outcome
1418
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
1419
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
1420
- - Fix: open Shortcuts.app, run the Shortcut the error names (`Apple Notes MCP - Native Tags` or `Apple Notes MCP - Background Operations v5`) 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
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
1421
1484
  - Read the exact note before retrying — the write may have landed
1422
1485
 
1423
1486
  ### "Note not found"
package/build/index.js CHANGED
@@ -39975,10 +39975,8 @@ import { homedir as homedir4 } from "os";
39975
39975
  import { join as join4 } from "path";
39976
39976
  var FIELD_SEP = "";
39977
39977
  var RECORD_SEP = "";
39978
- var AS_FIELD_SEP = "(ASCII character 31)";
39979
- var AS_RECORD_SEP = "(ASCII character 30)";
39980
- var AS_FIELD_SEP_LOCAL = "(character id 31)";
39981
- var AS_RECORD_SEP_LOCAL = "(character id 30)";
39978
+ var AS_FIELD_SEP = "(character id 31)";
39979
+ var AS_RECORD_SEP = "(character id 30)";
39982
39980
  var DEFAULT_EXPORT_PAGE_SIZE = 50;
39983
39981
  var DEFAULT_EXPORT_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
39984
39982
  function exportMaxResponseBytes(env = process.env) {
@@ -40817,11 +40815,11 @@ var AppleNotesManager = class {
40817
40815
  end try
40818
40816
  ${countGuard("noteIds")}
40819
40817
  repeat with i from 1 to count of noteNames
40820
- set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP_LOCAL} & (item i of noteIds)
40818
+ set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP} & (item i of noteIds)
40821
40819
  end repeat
40822
40820
  end if
40823
- set AppleScript's text item delimiters to ${AS_RECORD_SEP_LOCAL}
40824
- return (totalCount as text) & ${AS_RECORD_SEP_LOCAL} & (resultList as text)
40821
+ set AppleScript's text item delimiters to ${AS_RECORD_SEP}
40822
+ return (totalCount as text) & ${AS_RECORD_SEP} & (resultList as text)
40825
40823
  `;
40826
40824
  }
40827
40825
  const dateFetch = dateSetup ? `set noteDates to modification date of ${fullSource}
@@ -40838,9 +40836,9 @@ var AppleNotesManager = class {
40838
40836
  ${dateFetch}${countGuard("noteIds")}
40839
40837
  ${dateCountGuard}set resultList to {}
40840
40838
  repeat with i from 1 to count of noteNames
40841
- ${dateGuardOpen}set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP_LOCAL} & (item i of noteIds)${dateGuardClose}
40839
+ ${dateGuardOpen}set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP} & (item i of noteIds)${dateGuardClose}
40842
40840
  end repeat
40843
- set AppleScript's text item delimiters to ${AS_RECORD_SEP_LOCAL}
40841
+ set AppleScript's text item delimiters to ${AS_RECORD_SEP}
40844
40842
  return resultList as text
40845
40843
  `;
40846
40844
  }
@@ -40975,11 +40973,11 @@ var AppleNotesManager = class {
40975
40973
  if (item i of noteShared) is true then
40976
40974
  set cd to item i of noteCreated
40977
40975
  set md to item i of noteModified
40978
- set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP_LOCAL} & (item i of noteIds) & ${AS_FIELD_SEP_LOCAL} & ${asDatePartsExpr("cd")} & ${AS_FIELD_SEP_LOCAL} & ${asDatePartsExpr("md")} & ${AS_FIELD_SEP_LOCAL} & "true" & ${AS_FIELD_SEP_LOCAL} & ((item i of noteLocked) as text)
40976
+ set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP} & (item i of noteIds) & ${AS_FIELD_SEP} & ${asDatePartsExpr("cd")} & ${AS_FIELD_SEP} & ${asDatePartsExpr("md")} & ${AS_FIELD_SEP} & "true" & ${AS_FIELD_SEP} & ((item i of noteLocked) as text)
40979
40977
  end if
40980
40978
  end repeat
40981
40979
  end if
40982
- set AppleScript's text item delimiters to ${AS_RECORD_SEP_LOCAL}
40980
+ set AppleScript's text item delimiters to ${AS_RECORD_SEP}
40983
40981
  return resultList as text
40984
40982
  `
40985
40983
  );
@@ -42923,6 +42921,8 @@ function comparableVisibleText(html) {
42923
42921
  var BACKGROUND_SHORTCUT = "Apple Notes MCP - Background Operations v5";
42924
42922
  var backgroundStatus = () => nativeTagsStatus(process.env.APPLE_NOTES_MCP_BACKGROUND_SHORTCUT || BACKGROUND_SHORTCUT);
42925
42923
  var nativeTagBridgeStatus = () => nativeTagsStatus();
42924
+ var MARKDOWN_NOTE_SHORTCUT = "Apple Notes MCP - Create Markdown Note";
42925
+ var markdownNoteStatus = () => nativeTagsStatus(process.env.APPLE_NOTES_MCP_MARKDOWN_SHORTCUT || MARKDOWN_NOTE_SHORTCUT);
42926
42926
  function readBackgroundSnapshot(manager, id2) {
42927
42927
  if (!/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i.test(id2))
42928
42928
  throw new Error("Exact note ID required");
@@ -42974,6 +42974,17 @@ var NATIVE_APPEND_ELEMENTS = [
42974
42974
  ];
42975
42975
  var NATIVE_APPEND_SPAN_STYLE = /^font-size\s*:\s*\d{1,3}(?:\.\d+)?(?:px|pt)\s*;?$/i;
42976
42976
  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.`;
42977
+ var UNDERSCORES_OUTSIDE_A_WORD = "underscores outside a word";
42978
+ var UNMODELED_MARKDOWN = [
42979
+ [/(?<![\p{L}\p{N}])_|_(?![\p{L}\p{N}])/mu, UNDERSCORES_OUTSIDE_A_WORD],
42980
+ [/\\[!-/:-@[-`{-~]/m, "backslash escapes"],
42981
+ [/&(?:#\d+|#x[0-9a-f]+|[a-z][a-z0-9]*);/im, "character references"],
42982
+ [/^ {0,3}(?:=+|-+|(?:\*[ \t]*){3,})[ \t]*$/m, "underline or rule lines"],
42983
+ [/^ {1,3}(?:#|[-+*][ \t]|\d+[.)][ \t])/m, "indented headings or list items"],
42984
+ [/^\d+\)[ \t]/m, "`1)` lists"],
42985
+ [/^#{1,3}[ \t].*[ \t]#+[ \t]*$/m, "closing # sequences"],
42986
+ [/\[[^\]\n]*[*_][^\]\n]*\]\(/m, "formatting inside link labels"]
42987
+ ];
42977
42988
  function validateAppendContent(content, format) {
42978
42989
  if (!content || content.length > 1024 * 1024 || content.includes("\0"))
42979
42990
  throw new Error("Invalid append content (limit 1 MiB)");
@@ -43003,11 +43014,18 @@ function validateAppendContent(content, format) {
43003
43014
  }
43004
43015
  if (/<!--|<!|<\?|<[^>]*$/u.test(content)) throw new Error("Unsupported HTML markup");
43005
43016
  }
43006
- if (format === "markdown" && /!\[|<\/?[a-z]|\]\(\s*(?:javascript|data|file):/i.test(content))
43007
- throw new Error("Markdown images, raw HTML and local/executable links are unsupported");
43017
+ if (format === "markdown") {
43018
+ if (/!\[|<\/?[a-z]|\]\(\s*(?:javascript|data|file):/i.test(content))
43019
+ throw new Error("Markdown images, raw HTML and local/executable links are unsupported");
43020
+ const withoutLinkDestinations = content.replace(/(\[[^\]\n]+\])\([^()\s]+\)/g, "$1()");
43021
+ for (const [pattern, name] of UNMODELED_MARKDOWN)
43022
+ if (pattern.test(name === UNDERSCORES_OUTSIDE_A_WORD ? withoutLinkDestinations : content))
43023
+ throw new Error(
43024
+ `Markdown cannot use ${name}; Notes would change that text, so the result could not be verified`
43025
+ );
43026
+ }
43008
43027
  }
43009
- function runBackgroundShortcut(input) {
43010
- const status = backgroundStatus();
43028
+ function runBackgroundShortcut(input, status = backgroundStatus()) {
43011
43029
  if (!status.installed)
43012
43030
  throw new Error(
43013
43031
  `Install the supplied "${status.shortcut}" Shortcut once; Shortcuts must list it exactly once`
@@ -43109,6 +43127,11 @@ function assertPreserved(before, after, options = {}) {
43109
43127
  }
43110
43128
  }
43111
43129
  }
43130
+ function describeTransportFailure(error2) {
43131
+ const detail = error2;
43132
+ const named = detail?.shortcut ? `the "${detail.shortcut}" Shortcut` : "the background Shortcut";
43133
+ 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)}`;
43134
+ }
43112
43135
  function mutateBackground(request, operation, data, verify, deps) {
43113
43136
  if (request.scopeText.length < 12 || request.scopeText.length > 500 || /[\r\n\0]/u.test(request.scopeText))
43114
43137
  throw new Error("Use a distinctive existing single-line scope of 12\u2013500 characters");
@@ -43128,9 +43151,7 @@ function mutateBackground(request, operation, data, verify, deps) {
43128
43151
  deps.run({ ...data, operation, title: before.title, scopeText: request.scopeText });
43129
43152
  } catch (error2) {
43130
43153
  transportUncertain = true;
43131
- const detail = error2;
43132
- const named = detail?.shortcut ? `the "${detail.shortcut}" Shortcut` : "the background Shortcut";
43133
- transportMessage = 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)}`;
43154
+ transportMessage = describeTransportFailure(error2);
43134
43155
  }
43135
43156
  const after = deps.read(request.id);
43136
43157
  try {
@@ -43218,6 +43239,109 @@ function setNativeTag(manager, request) {
43218
43239
  deps
43219
43240
  );
43220
43241
  }
43242
+ var literalMarkdown = (text) => text.replace(/[!-/:-@[-`{-~]/g, "\\$&");
43243
+ function headingLevels(html) {
43244
+ return [...html.matchAll(/<h([1-3])\b[^>]*>([\s\S]*?)<\/h\1>/gi)].filter((match) => comparableVisibleText(match[2])).map((match) => Number(match[1]));
43245
+ }
43246
+ function createMarkdownNote(manager, request, run = runBackgroundShortcut) {
43247
+ if (/[\r\n\0]/u.test(request.title)) throw new Error("A Markdown note title must be one line");
43248
+ validateAppendContent(request.content, "markdown");
43249
+ const bodyHtml = appendMarkdownHtml(request.content);
43250
+ if (request.folder) buildFolderReference(request.folder);
43251
+ const expectedText = `${request.title} ${comparableVisibleText(bodyHtml)}`.replace(/\s+/gu, " ").trim();
43252
+ const expectedLevels = [1, ...headingLevels(bodyHtml)].join();
43253
+ const status = markdownNoteStatus();
43254
+ if (!status.installed)
43255
+ throw new Error(
43256
+ `Install the supplied "${status.shortcut}" Shortcut once; Shortcuts must list it exactly once`
43257
+ );
43258
+ const segments = (path4) => JSON.stringify(splitFolderPath(path4).map((part) => part.toLocaleLowerCase()));
43259
+ if (request.folder) {
43260
+ const wanted = segments(request.folder);
43261
+ if (!manager.listAccounts().some(
43262
+ (account) => account.defaultFolder && manager.listFolders(account.name).some((folder) => segments(folder.name) === wanted)
43263
+ ))
43264
+ throw new Error(
43265
+ `Folder "${request.folder}" does not exist; create it with create-folder first. Nothing was created`
43266
+ );
43267
+ }
43268
+ const defaultFolderNotes = () => new Map(
43269
+ manager.listAccounts().flatMap(
43270
+ (account) => account.defaultFolder ? manager.listNoteRefs(account.name, account.defaultFolder).map((note) => [note.id, account.name]) : []
43271
+ )
43272
+ );
43273
+ const before = defaultFolderNotes();
43274
+ let transportMessage = "";
43275
+ try {
43276
+ run(
43277
+ {
43278
+ operation: "create-markdown",
43279
+ text: `# ${literalMarkdown(request.title)}
43280
+
43281
+ ${request.content}`
43282
+ },
43283
+ status
43284
+ );
43285
+ } catch (error2) {
43286
+ transportMessage = describeTransportFailure(error2);
43287
+ }
43288
+ const reason = (error2) => error2 instanceof Error ? error2.message : String(error2);
43289
+ let candidates = [];
43290
+ try {
43291
+ const created = [...defaultFolderNotes()].filter(([noteId3]) => !before.has(noteId3));
43292
+ candidates = created.map(([noteId3]) => noteId3);
43293
+ const failures = [];
43294
+ const verified = created.flatMap(([noteId3, account2]) => {
43295
+ try {
43296
+ const note2 = readBackgroundSnapshot(manager, noteId3);
43297
+ if (note2.rich.text.replace(/[\s\ufffc]+/gu, " ").trim() !== expectedText)
43298
+ throw new Error("Note text not verified");
43299
+ if (headingLevels(note2.html).join() !== expectedLevels)
43300
+ throw new Error("Heading styles not verified");
43301
+ assertAppendedHtmlLinks(0, note2.rich.links, bodyHtml);
43302
+ return [{ id: noteId3, account: account2, note: note2 }];
43303
+ } catch (error2) {
43304
+ failures.push(`${noteId3}: ${reason(error2)}`);
43305
+ return [];
43306
+ }
43307
+ });
43308
+ if (verified.length !== 1)
43309
+ throw new Error(
43310
+ 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"
43311
+ );
43312
+ const { id: id2, account } = verified[0];
43313
+ candidates = [id2];
43314
+ let { note } = verified[0];
43315
+ if (request.folder) {
43316
+ const wanted = segments(request.folder);
43317
+ if (!manager.listFolders(account).some((folder) => segments(folder.name) === wanted))
43318
+ throw new Error(
43319
+ `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`
43320
+ );
43321
+ if (!manager.moveNoteById(id2, request.folder, account))
43322
+ throw new Error(
43323
+ `created and verified in the ${account} default folder, but not moved to "${request.folder}"; use move-note instead of creating it again`
43324
+ );
43325
+ note = readBackgroundSnapshot(manager, id2);
43326
+ }
43327
+ return {
43328
+ ok: true,
43329
+ id: id2,
43330
+ title: request.title,
43331
+ folder: request.folder,
43332
+ account,
43333
+ contentHash: note.hash,
43334
+ verified: true,
43335
+ ...transportMessage ? {
43336
+ transportWarning: "Transport was uncertain; exact-ID readback verified the requested result"
43337
+ } : {}
43338
+ };
43339
+ } catch (error2) {
43340
+ throw new Error(
43341
+ `Operation outcome uncertain; ${candidates.length ? `read ${candidates.length === 1 ? "note" : "notes"} ${candidates.join(", ")}` : "search for the title"} before any retry: ${reason(error2)}${transportMessage ? `; ${transportMessage}` : ""}`
43342
+ );
43343
+ }
43344
+ }
43221
43345
 
43222
43346
  // src/tools/doctor.ts
43223
43347
  function runDoctor(manager) {
@@ -43256,10 +43380,11 @@ function runDoctor(manager) {
43256
43380
  (name) => nativeTagsStatus(name)
43257
43381
  );
43258
43382
  const missing = bridgeStatuses.filter((status) => !status.installed);
43383
+ const markdown = markdownBridgeDetail();
43259
43384
  checks.push({
43260
43385
  name: "Native write Shortcuts",
43261
43386
  status: missing.length ? "warn" : "ok",
43262
- detail: missing.length ? `missing: ${missing.map((status) => status.shortcut).join(", ")}. Run apple-notes-mcp setup and approve Add Shortcut in macOS. ${consentReminder}` : `both native-write bridges are installed. ${consentReminder}`
43387
+ 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}`
43263
43388
  });
43264
43389
  } catch (error2) {
43265
43390
  checks.push({
@@ -43272,6 +43397,14 @@ function runDoctor(manager) {
43272
43397
  const healthy = !checks.some((c) => c.status === "fail");
43273
43398
  return { healthy, checks };
43274
43399
  }
43400
+ function markdownBridgeDetail() {
43401
+ const purpose = 'needed only for create-note format: "markdown" on macOS 26+';
43402
+ try {
43403
+ 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).`;
43404
+ } catch (error2) {
43405
+ return `Optional ${MARKDOWN_NOTE_SHORTCUT} bridge: could not inspect (${purpose}): ${String(error2)}.`;
43406
+ }
43407
+ }
43275
43408
  function checkNodeRuntimeSignature() {
43276
43409
  const name = "Node runtime signature";
43277
43410
  try {
@@ -43894,7 +44027,8 @@ var VERIFIED_BACKGROUND = /* @__PURE__ */ new Set([
43894
44027
  "insert-note-link",
43895
44028
  "set-note-pinned",
43896
44029
  "remove-native-tags",
43897
- "replace-native-tag"
44030
+ "replace-native-tag",
44031
+ "create-note-markdown"
43898
44032
  ]);
43899
44033
  var LIVE_VALIDATION_BLOCKERS = {};
43900
44034
  var signingRefusal = "Installed Shortcuts refuses to sign this Notes action (unsupported features); no background fallback is enabled";
@@ -43982,16 +44116,24 @@ function registerNativeOperations(server2, manager) {
43982
44116
  "set-note-pinned",
43983
44117
  "remove-native-tags",
43984
44118
  "replace-native-tag",
43985
- "insert-note-link"
44119
+ "insert-note-link",
44120
+ "create-note-markdown"
43986
44121
  ];
43987
44122
  let tagBridgeInstalled = false;
43988
44123
  try {
43989
44124
  tagBridgeInstalled = nativeTagBridgeStatus().installed;
43990
44125
  } catch {
43991
44126
  }
44127
+ let markdownBridgeInstalled = false;
44128
+ try {
44129
+ markdownBridgeInstalled = markdownNoteStatus().installed;
44130
+ } catch {
44131
+ }
44132
+ const installed = (name) => name === "create-note-markdown" ? markdownBridgeInstalled : bridge.installed;
43992
44133
  return {
43993
44134
  bridge,
43994
44135
  nativeTagBridgeInstalled: tagBridgeInstalled,
44136
+ markdownNoteBridgeInstalled: markdownBridgeInstalled,
43995
44137
  mode: "background-only",
43996
44138
  operations: Object.fromEntries(
43997
44139
  native.map((name) => [
@@ -43999,8 +44141,8 @@ function registerNativeOperations(server2, manager) {
43999
44141
  {
44000
44142
  implemented: true,
44001
44143
  verified: VERIFIED_BACKGROUND.has(name),
44002
- available: VERIFIED_BACKGROUND.has(name) && (!native.includes(name) || bridge.installed) && (name !== "replace-native-tag" || tagBridgeInstalled),
44003
- 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
44144
+ available: VERIFIED_BACKGROUND.has(name) && installed(name) && (name !== "replace-native-tag" || tagBridgeInstalled),
44145
+ 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
44004
44146
  }
44005
44147
  ])
44006
44148
  ),
@@ -44217,13 +44359,21 @@ function registerNativeOperations(server2, manager) {
44217
44359
  // src/setupShortcuts.ts
44218
44360
  import { spawnSync as spawnSync2 } from "node:child_process";
44219
44361
  import { existsSync as existsSync7 } from "node:fs";
44362
+ import { release } from "node:os";
44220
44363
  import { dirname as dirname2, resolve as resolve2 } from "node:path";
44221
44364
  import { fileURLToPath } from "node:url";
44365
+ var OPTIONAL_BRIDGE_NOTE = "(optional \u2014 needed only for create-note format: markdown, macOS 26+)";
44366
+ var MARKDOWN_MIN_DARWIN_MAJOR = 25;
44222
44367
  var shortcutFiles = [
44223
44368
  { name: NATIVE_TAGS_SHORTCUT, file: "Apple Notes MCP - Native Tags.shortcut" },
44224
44369
  {
44225
44370
  name: BACKGROUND_SHORTCUT,
44226
44371
  file: "Apple Notes MCP - Background Operations v5.shortcut"
44372
+ },
44373
+ {
44374
+ name: MARKDOWN_NOTE_SHORTCUT,
44375
+ file: "Apple Notes MCP - Create Markdown Note.shortcut",
44376
+ optional: true
44227
44377
  }
44228
44378
  ];
44229
44379
  function setupShortcuts(checkOnly, dependencies = {}) {
@@ -44234,7 +44384,9 @@ function setupShortcuts(checkOnly, dependencies = {}) {
44234
44384
  return result.status === 0 ? { ok: true } : { ok: false, error: result.stderr || result.error?.message || "open failed" };
44235
44385
  });
44236
44386
  const baseDirectory = dependencies.baseDirectory || resolve2(dirname2(fileURLToPath(import.meta.url)), "../shortcuts");
44237
- const items = shortcutFiles.map(({ name, file }) => {
44387
+ const osRelease = (dependencies.osRelease || release)();
44388
+ const darwinMajor = Number.parseInt(osRelease.split(".")[0], 10);
44389
+ const items = shortcutFiles.map(({ name, file, optional: optional2 }) => {
44238
44390
  const path4 = resolve2(baseDirectory, file);
44239
44391
  let installed = false;
44240
44392
  let identifier;
@@ -44247,27 +44399,41 @@ function setupShortcuts(checkOnly, dependencies = {}) {
44247
44399
  error2 = cause instanceof Error ? cause.message : String(cause);
44248
44400
  }
44249
44401
  let opened = false;
44402
+ let skipped;
44250
44403
  if (!installed && !checkOnly) {
44251
- if (!exists(path4)) error2 = `Packaged Shortcut is missing: ${path4}`;
44404
+ if (optional2 && !(darwinMajor >= MARKDOWN_MIN_DARWIN_MAJOR))
44405
+ skipped = `requires macOS 26 or later (this Mac reports Darwin ${osRelease})`;
44406
+ else if (!exists(path4)) error2 = `Packaged Shortcut is missing: ${path4}`;
44252
44407
  else {
44253
44408
  const result = open(path4);
44254
44409
  opened = result.ok;
44255
44410
  if (!result.ok) error2 = result.error || `Could not open ${file}`;
44256
44411
  }
44257
44412
  }
44258
- return { name, installed, identifier, file: path4, opened, ...error2 ? { error: error2 } : {} };
44413
+ return {
44414
+ name,
44415
+ installed,
44416
+ identifier,
44417
+ file: path4,
44418
+ opened,
44419
+ ...optional2 ? { optional: optional2 } : {},
44420
+ ...skipped ? { skipped } : {},
44421
+ ...error2 ? { error: error2 } : {}
44422
+ };
44259
44423
  });
44260
- return { ready: items.every((item) => item.installed), checkOnly, items };
44424
+ return { ready: items.every((item) => item.optional || item.installed), checkOnly, items };
44261
44425
  }
44262
44426
  function formatShortcutSetup(report) {
44263
44427
  const lines = ["Apple Notes MCP Shortcut setup", ""];
44264
44428
  for (const item of report.items) {
44265
- if (item.installed) lines.push(`\u2713 ${item.name} (${item.identifier})`);
44266
- else if (item.opened) lines.push(`\u2192 ${item.name}: confirm \u201CAdd Shortcut\u201D in macOS`);
44267
- else lines.push(`\u2717 ${item.name}: ${item.error || "not installed"}`);
44429
+ const note = item.optional ? ` ${OPTIONAL_BRIDGE_NOTE}` : "";
44430
+ if (item.installed) lines.push(`\u2713 ${item.name} (${item.identifier})${note}`);
44431
+ else if (item.opened) lines.push(`\u2192 ${item.name}: confirm \u201CAdd Shortcut\u201D in macOS${note}`);
44432
+ else if (item.skipped) lines.push(`\u2013 ${item.name}: skipped, ${item.skipped}${note}`);
44433
+ else lines.push(`\u2717 ${item.name}: ${item.error || "not installed"}${note}`);
44268
44434
  }
44269
44435
  lines.push("");
44270
- if (report.ready) lines.push("Both Shortcut bridges are installed.");
44436
+ if (report.ready) lines.push("Both required Shortcut bridges are installed.");
44271
44437
  else if (report.checkOnly) lines.push("Run `apple-notes-mcp setup` to open missing workflows.");
44272
44438
  else
44273
44439
  lines.push(
@@ -44383,9 +44549,11 @@ registerTool(
44383
44549
  content: external_exports.string().min(1, "Content is required").max(MAX.CONTENT).describe(
44384
44550
  '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.'
44385
44551
  ),
44386
- format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
44552
+ format: external_exports.enum(["plaintext", "html", "markdown"]).optional().default("plaintext").describe(
44553
+ "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)"
44554
+ ),
44387
44555
  tags: external_exports.array(external_exports.string().max(MAX.TAG)).max(MAX.TAGS).optional().describe(
44388
- "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."
44556
+ "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."
44389
44557
  ),
44390
44558
  folder: external_exports.string().max(MAX.FOLDER).optional().describe(
44391
44559
  "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."
@@ -44405,6 +44573,19 @@ registerTool(
44405
44573
  }
44406
44574
  },
44407
44575
  withErrorHandling(({ title, content, format = "plaintext", tags = [], folder, account }) => {
44576
+ if (format === "markdown") {
44577
+ if (account)
44578
+ return errorResponse(
44579
+ "Markdown notes are created in the iCloud account, the only one where Notes interprets Markdown; omit account"
44580
+ );
44581
+ if (tags.length)
44582
+ return errorResponse(
44583
+ 'tags are not supported with format "markdown"; create the note without tags, then add them with add-native-tags using the returned id'
44584
+ );
44585
+ requireValidated("create-note-markdown");
44586
+ const result = createMarkdownNote(notesManager, { title, content, folder });
44587
+ return successResponse(`Note created from Markdown: "${title}" [id: ${result.id}]`, result);
44588
+ }
44408
44589
  const note = notesManager.createNote(title, content, tags, folder, account, format);
44409
44590
  if (!note) {
44410
44591
  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.15",
3
+ "version": "2.8.17",
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>
@@ -49,3 +49,35 @@ Open the signed file and confirm **Add Shortcut** once, then run it once in the
49
49
  foreground in Shortcuts.app and choose **Always Allow** (see above). Do not install obsolete
50
50
  workflow versions with the same display name; the server resolves one exact
51
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
+ ```