apple-notes-mcp 2.9.4 → 2.9.5
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 +53 -2
- package/build/index.js +263 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -746,6 +746,57 @@ HTML.
|
|
|
746
746
|
|
|
747
747
|
---
|
|
748
748
|
|
|
749
|
+
#### `list-note-links`
|
|
750
|
+
|
|
751
|
+
Lists links, read-only from the NoteStore database, in one note (`id`) or
|
|
752
|
+
across a `folder`, an `account`, or the whole library (no selector). Each link
|
|
753
|
+
has a `kind`:
|
|
754
|
+
|
|
755
|
+
| Kind | What it is | Where Notes stores it |
|
|
756
|
+
|------|------------|-----------------------|
|
|
757
|
+
| `inline` | A hyperlink on text | Inside the note body |
|
|
758
|
+
| `card` | A rich link preview | An attachment row with the URL and title |
|
|
759
|
+
| `note` | A native link chip to another note | An inline-attachment row with a Notes deep link |
|
|
760
|
+
| `section` | A native link chip to a heading or paragraph | Same, with `paragraphID` in the deep link |
|
|
761
|
+
|
|
762
|
+
Each row has `url`, `text` (label), `linkSafe`, `targetNote` and `paragraphId`
|
|
763
|
+
for Notes deep links, `section` for section chips, and for cards
|
|
764
|
+
`attachmentId` and `previewPath` (Notes' largest cached preview image, found
|
|
765
|
+
the same way [`list-attachments`](#list-attachments) finds it, or null when
|
|
766
|
+
Notes has none). Each row also names its source: `noteId`, `noteIdentifier`,
|
|
767
|
+
`noteTitle`, `noteModified`, `folder`, `folderPath` (as `list-folders` prints
|
|
768
|
+
it), `account`, and `accountIdentifier`. When bodies were decoded, `start` and
|
|
769
|
+
`blockIndex` give the link's position, and `inBody: false` marks a card or chip
|
|
770
|
+
row with no marker left in the body.
|
|
771
|
+
|
|
772
|
+
Inline links need every body in scope decompressed and decoded, so a folder,
|
|
773
|
+
account, or library scan includes them only with `includeInline: true`
|
|
774
|
+
(slower). A single note always includes them. A `folder` scope includes its
|
|
775
|
+
subfolders unless `includeSubfolders` is false. Scans skip Recently Deleted
|
|
776
|
+
and folderless notes; a note requested by `id` is read wherever it is. Links
|
|
777
|
+
come newest-modified note first, in body order within a note.
|
|
778
|
+
|
|
779
|
+
**Requires:** Full Disk Access.
|
|
780
|
+
|
|
781
|
+
| Parameter | Type | Required | Description |
|
|
782
|
+
|-----------|------|----------|-------------|
|
|
783
|
+
| `id` | string | No | One exact note ID (x-coredata id, Notes UUID, or numeric key). Do not combine with `account` or `folder` |
|
|
784
|
+
| `account` | string | No | Account name (exact or unique-prefix match, as in the other tools) |
|
|
785
|
+
| `folder` | string | No | Folder name (any depth, must be unique) or path from the top level as `list-folders` prints it, such as `Work/Clients`; escape a literal slash as `\/` |
|
|
786
|
+
| `includeSubfolders` | boolean | No | With `folder`, also list notes in its subfolders (default true) |
|
|
787
|
+
| `includeInline` | boolean | No | Decode bodies for inline links (default true for `id`, false otherwise) |
|
|
788
|
+
| `kinds` | string[] | No | Only these kinds: `inline`, `card`, `note`, `section` |
|
|
789
|
+
| `offset` | number | No | First link to return (default 0). Use `page.nextOffset` |
|
|
790
|
+
| `limit` | number | No | Maximum links per page (default 200, max 2000) |
|
|
791
|
+
|
|
792
|
+
The response also reports `scope`, `counts` per kind, `notesInScope`,
|
|
793
|
+
`notesWithoutBody` (locked, empty or undecodable bodies when inline links were
|
|
794
|
+
requested), and `page`. A page stops early to stay under
|
|
795
|
+
`APPLE_NOTES_MCP_BLOCKS_MAX_BYTES`. An ambiguous folder name is refused with a
|
|
796
|
+
message that lists the matching paths.
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
749
800
|
#### `list-native-tags`
|
|
750
801
|
|
|
751
802
|
Lists actual native Notes tags. This differs from textual hashtag search. It is
|
|
@@ -2326,7 +2377,7 @@ All configuration is optional — the server works out of the box. Override beha
|
|
|
2326
2377
|
| `APPLE_NOTES_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-notes-mcp/config.json` | Path to the JSON config file (see below). |
|
|
2327
2378
|
| `APPLE_NOTES_MCP_TIMEOUT_MS` | `30000` (30 s) | Total AppleScript operation timeout, including retry attempts and delays. Raise it if full-library operations (large searches, exports) time out on a big Notes library. Per-call `timeoutMs` options still win, and a write tool's `timeoutSeconds` argument overrides it for that call. |
|
|
2328
2379
|
| `APPLE_NOTES_MCP_EXPORT_MAX_BYTES` | `8388608` (8 MB) | Largest response `export-notes-json` sends; a page closes early to stay under it. `export-notes-markdown` returns inline Markdown up to half of it. The default sits below the 10 MB per-message limit of MCP SDK stdio clients, which drop the connection on anything larger. Raise it only if your MCP client accepts bigger messages. |
|
|
2329
|
-
| `APPLE_NOTES_MCP_BLOCKS_MAX_BYTES` | `4194304` (4 MB) | Largest block payload one [`get-note-blocks`](#get-note-blocks) page returns; the page closes early to stay under it, and a single oversized paragraph comes back with `textOmitted: true`. [`get-note-structure`](#get-note-structure) also omits note text larger than this. A [`list-note-paragraphs`](#list-note-paragraphs) page also stops early to stay under it. |
|
|
2380
|
+
| `APPLE_NOTES_MCP_BLOCKS_MAX_BYTES` | `4194304` (4 MB) | Largest block payload one [`get-note-blocks`](#get-note-blocks) page returns; the page closes early to stay under it, and a single oversized paragraph comes back with `textOmitted: true`. [`get-note-structure`](#get-note-structure) also omits note text larger than this. A [`list-note-paragraphs`](#list-note-paragraphs) or [`list-note-links`](#list-note-links) page also stops early to stay under it. |
|
|
2330
2381
|
| `APPLE_NOTES_MCP_MAX_RETRIES` | `2` | Maximum attempts for a read-only AppleScript call that fails with a **transient** error (Notes.app busy / not responding / lost connection). `2` means one retry; set `1` to fail fast with no retries. Retries share the single `APPLE_NOTES_MCP_TIMEOUT_MS` budget rather than each getting a fresh one, and a retry is skipped when under a second of that budget remains — so this is a ceiling, not a guarantee. In particular a call that exhausts the budget with a **timeout** has no time left to retry by construction. Mutating operations run once because a timeout can occur after Notes.app applied the change. Non-transient errors (e.g. "note not found") never retry. |
|
|
2331
2382
|
| `APPLE_NOTES_MCP_RETRY_DELAY_MS` | `1000` (1 s) | Base delay before the first retry; subsequent retries back off exponentially (1s, 2s, 4s, ...). |
|
|
2332
2383
|
| `APPLE_NOTES_MCP_ENABLE_PRIVATE` | unset | Set to `1` to allow the opt-in [private helper](#private-helper-opt-in-unsupported-apple-api). Any other value keeps it off. |
|
|
@@ -2359,7 +2410,7 @@ MCP stores no secrets, but as a general rule keep only non-secret config here.
|
|
|
2359
2410
|
|
|
2360
2411
|
## Full Disk Access
|
|
2361
2412
|
|
|
2362
|
-
Several tools read directly from the Apple Notes SQLite database, which lives in a macOS-protected directory. Those tools require **Full Disk Access** for the process running the MCP server: `get-checklist-state`, `get-note-metadata`, `get-note-blocks`, `list-note-paragraphs`, `get-paragraph-link`, `get-note-structure`, `export-notes-markdown`, `export-notes-html`, `get-audio-transcripts`, `list-special-notes`, `list-native-tags`, `list-recent-notes`, `list-folder-tree`, `get-note-link`, the checklist annotations in `get-note-markdown`, `list-attachments` with `includePaths` or `firstImage`, `export-attachments`, `list-paper-attachments`, `export-paper-image`, and the database half of `get-sync-status`.
|
|
2413
|
+
Several tools read directly from the Apple Notes SQLite database, which lives in a macOS-protected directory. Those tools require **Full Disk Access** for the process running the MCP server: `get-checklist-state`, `get-note-metadata`, `get-note-blocks`, `list-note-paragraphs`, `get-paragraph-link`, `get-note-structure`, `list-note-links`, `export-notes-markdown`, `export-notes-html`, `get-audio-transcripts`, `list-special-notes`, `list-native-tags`, `list-recent-notes`, `list-folder-tree`, `get-note-link`, the checklist annotations in `get-note-markdown`, `list-attachments` with `includePaths` or `firstImage`, `export-attachments`, `list-paper-attachments`, `export-paper-image`, and the database half of `get-sync-status`.
|
|
2363
2414
|
|
|
2364
2415
|
> 📘 **For the full why-and-how walkthrough (which app to grant, verifying with `doctor`, graceful degradation), see the [Full Disk Access Setup Guide](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md).** The summary below is the quick version.
|
|
2365
2416
|
|
package/build/index.js
CHANGED
|
@@ -49582,6 +49582,212 @@ function readNoteStructure(id2, { dbPath: dbPath2 = NOTES_DB_PATH7, includeText
|
|
|
49582
49582
|
};
|
|
49583
49583
|
}
|
|
49584
49584
|
|
|
49585
|
+
// src/utils/noteLinkInventory.ts
|
|
49586
|
+
import { dirname as dirname5 } from "node:path";
|
|
49587
|
+
var BODY_BATCH = 100;
|
|
49588
|
+
function matchFolder(folders, wanted, account) {
|
|
49589
|
+
const paths = folderPaths(folders);
|
|
49590
|
+
const segments = splitFolderPath(wanted);
|
|
49591
|
+
const key = (parts) => parts.join("\0");
|
|
49592
|
+
const pool = folders.filter(
|
|
49593
|
+
(f) => !f.tombstoned && !f.trash && (account === void 0 || f.account === account)
|
|
49594
|
+
);
|
|
49595
|
+
const byPath = segments.length === 1 ? pool.filter((f) => f.name === segments[0]) : pool.filter((f) => key(splitFolderPath(paths.get(f.pk))) === key(segments));
|
|
49596
|
+
const matches = byPath.length ? byPath : pool.filter((f) => f.name === wanted);
|
|
49597
|
+
if (!matches.length) throw new NoteStoreError(`No folder matches "${wanted}".`, "invalid_input");
|
|
49598
|
+
if (matches.length > 1)
|
|
49599
|
+
throw new NoteStoreError(
|
|
49600
|
+
`Folder "${wanted}" is ambiguous; use one of these paths: ${matches.map((f) => paths.get(f.pk)).join(", ")}.`,
|
|
49601
|
+
"invalid_input"
|
|
49602
|
+
);
|
|
49603
|
+
return { folder: matches[0], path: paths.get(matches[0].pk) };
|
|
49604
|
+
}
|
|
49605
|
+
function inventorySql(columns) {
|
|
49606
|
+
requireColumns(columns, ["ZFOLDER"], "list-note-links");
|
|
49607
|
+
const c = (alias, name) => col(columns, alias, name);
|
|
49608
|
+
const subtree = columns.has("ZPARENT") ? `WITH RECURSIVE sub(pk) AS (SELECT @folder UNION
|
|
49609
|
+
SELECT x.Z_PK FROM ZICCLOUDSYNCINGOBJECT x JOIN sub ON x.ZPARENT = sub.pk
|
|
49610
|
+
WHERE @subfolders <> 0 AND x.Z_ENT = ${entity("ICFolder")})
|
|
49611
|
+
SELECT pk FROM sub` : "SELECT @folder";
|
|
49612
|
+
const scopeWhere = `n.Z_ENT = ${entity("ICNote")} AND (
|
|
49613
|
+
(@note <> 0 AND n.Z_PK = @note) OR
|
|
49614
|
+
(@note = 0 AND ${activeNoteSql(columns, "n", "f")}
|
|
49615
|
+
AND (@account = 0 OR a.Z_PK = @account)
|
|
49616
|
+
AND (@folder = 0 OR n.ZFOLDER IN (${subtree}))))`;
|
|
49617
|
+
const scope2 = `SELECT n.Z_PK FROM ZICCLOUDSYNCINGOBJECT n ${folderAccountJoins(columns)} WHERE ${scopeWhere}`;
|
|
49618
|
+
return {
|
|
49619
|
+
rows: [
|
|
49620
|
+
"BEGIN;",
|
|
49621
|
+
`SELECT json_object('k', 'note', 'pk', n.Z_PK, 'identifier', ${c("n", "ZIDENTIFIER")},
|
|
49622
|
+
'title', ${c("n", "ZTITLE1")}, 'folder', n.ZFOLDER, 'account', a.Z_PK,
|
|
49623
|
+
'modified', ${c("n", "ZMODIFICATIONDATE1")})
|
|
49624
|
+
FROM ZICCLOUDSYNCINGOBJECT n ${folderAccountJoins(columns)} WHERE ${scopeWhere};`,
|
|
49625
|
+
`SELECT json_object('k', 'card', 'pk', att.Z_PK, 'note', ${c("att", "ZNOTE")},
|
|
49626
|
+
'identifier', ${c("att", "ZIDENTIFIER")}, 'url', ${c("att", "ZURLSTRING")},
|
|
49627
|
+
'title', ${c("att", "ZTITLE")})
|
|
49628
|
+
FROM ZICCLOUDSYNCINGOBJECT att
|
|
49629
|
+
WHERE att.Z_ENT = ${entity("ICAttachment")} AND ${notTombstonedSql(columns, "att")}
|
|
49630
|
+
AND ${c("att", "ZTYPEUTI")} LIKE 'public.url%' AND ${c("att", "ZURLSTRING")} IS NOT NULL
|
|
49631
|
+
AND ${c("att", "ZNOTE")} IN (${scope2});`,
|
|
49632
|
+
`SELECT json_object('k', 'chip', 'note', ${c("i", "ZNOTE1")},
|
|
49633
|
+
'identifier', ${c("i", "ZIDENTIFIER")}, 'token', ${c("i", "ZTOKENCONTENTIDENTIFIER")},
|
|
49634
|
+
'alt', ${c("i", "ZALTTEXT")})
|
|
49635
|
+
FROM ZICCLOUDSYNCINGOBJECT i
|
|
49636
|
+
WHERE i.Z_ENT = ${entity("ICInlineAttachment")} AND ${notTombstonedSql(columns, "i")}
|
|
49637
|
+
AND ${c("i", "ZTYPEUTI1")} = '${NOTE_LINK_UTI}' AND ${c("i", "ZNOTE1")} IN (${scope2});`,
|
|
49638
|
+
"COMMIT;"
|
|
49639
|
+
].join("\n"),
|
|
49640
|
+
bodies: `SELECT json_object('note', d.ZNOTE, 'data', hex(d.ZDATA),
|
|
49641
|
+
'encrypted', d.ZCRYPTOINITIALIZATIONVECTOR IS NOT NULL)
|
|
49642
|
+
FROM ZICNOTEDATA d WHERE d.ZNOTE IN (${scope2}) AND d.ZNOTE > @after
|
|
49643
|
+
ORDER BY d.ZNOTE LIMIT ${BODY_BATCH};`
|
|
49644
|
+
};
|
|
49645
|
+
}
|
|
49646
|
+
function decodeBodies(dbPath2, sql, params) {
|
|
49647
|
+
const docs = /* @__PURE__ */ new Map();
|
|
49648
|
+
let after = 0;
|
|
49649
|
+
for (; ; ) {
|
|
49650
|
+
const rows = parseJsonLines(
|
|
49651
|
+
runReadOnlySql(dbPath2, sql, { ...params, after: { int: after } })
|
|
49652
|
+
);
|
|
49653
|
+
for (const row of rows) {
|
|
49654
|
+
after = Math.max(after, row.note);
|
|
49655
|
+
if (row.encrypted || !row.data) continue;
|
|
49656
|
+
try {
|
|
49657
|
+
docs.set(row.note, decodeCompressedNoteBlocks(Buffer.from(row.data, "hex")));
|
|
49658
|
+
} catch {
|
|
49659
|
+
}
|
|
49660
|
+
}
|
|
49661
|
+
if (rows.length < BODY_BATCH) return docs;
|
|
49662
|
+
}
|
|
49663
|
+
}
|
|
49664
|
+
var KIND_ORDER2 = { inline: 0, card: 1, note: 2, section: 3 };
|
|
49665
|
+
function listNoteLinks(options = {}) {
|
|
49666
|
+
const { dbPath: dbPath2 = NOTES_DB_PATH7, offset = 0, limit = 200, maxBytes = 4 * 1024 * 1024 } = options;
|
|
49667
|
+
if (options.id && (options.account || options.folder))
|
|
49668
|
+
throw new NoteStoreError("Pass either id or account/folder, not both.", "invalid_input");
|
|
49669
|
+
const notePk = options.id ? parseNoteId2(options.id).pk : 0;
|
|
49670
|
+
const columns = readColumns(dbPath2);
|
|
49671
|
+
const sql = inventorySql(columns);
|
|
49672
|
+
const context = readStoreContext(dbPath2, columns);
|
|
49673
|
+
const paths = folderPaths(context.folders);
|
|
49674
|
+
const scope2 = {};
|
|
49675
|
+
let accountPk = 0;
|
|
49676
|
+
let folderPk = 0;
|
|
49677
|
+
const includeSubfolders = options.includeSubfolders ?? true;
|
|
49678
|
+
if (options.account) {
|
|
49679
|
+
const account = resolveAccountName(context.accounts, options.account);
|
|
49680
|
+
accountPk = account.pk;
|
|
49681
|
+
scope2.account = account.name;
|
|
49682
|
+
scope2.accountIdentifier = account.identifier;
|
|
49683
|
+
}
|
|
49684
|
+
if (options.folder) {
|
|
49685
|
+
const { folder, path: path10 } = matchFolder(context.folders, options.folder, accountPk || void 0);
|
|
49686
|
+
folderPk = folder.pk;
|
|
49687
|
+
scope2.folder = folder.name;
|
|
49688
|
+
scope2.folderPath = path10;
|
|
49689
|
+
scope2.includeSubfolders = includeSubfolders;
|
|
49690
|
+
}
|
|
49691
|
+
if (options.id) scope2.note = options.id;
|
|
49692
|
+
const params = {
|
|
49693
|
+
note: { int: notePk },
|
|
49694
|
+
account: { int: accountPk },
|
|
49695
|
+
folder: { int: folderPk },
|
|
49696
|
+
subfolders: { int: includeSubfolders ? 1 : 0 }
|
|
49697
|
+
};
|
|
49698
|
+
const rows = parseJsonLines(runReadOnlySql(dbPath2, sql.rows, params));
|
|
49699
|
+
const notes = rows.filter((row) => row.k === "note");
|
|
49700
|
+
if (options.id && !notes.length)
|
|
49701
|
+
throw new NoteStoreError(`No note found for ID "${options.id}".`, "invalid_input");
|
|
49702
|
+
const includeInline = options.includeInline ?? Boolean(options.id);
|
|
49703
|
+
const docs = includeInline ? decodeBodies(dbPath2, sql.bodies, params) : /* @__PURE__ */ new Map();
|
|
49704
|
+
const noteById = new Map(notes.map((note) => [note.pk, note]));
|
|
49705
|
+
const accountById = new Map(context.accounts.map((account) => [account.pk, account]));
|
|
49706
|
+
const folderById = new Map(context.folders.map((folder) => [folder.pk, folder]));
|
|
49707
|
+
const source = (pk) => {
|
|
49708
|
+
const note = noteById.get(pk);
|
|
49709
|
+
const account = accountById.get(note.account ?? -1);
|
|
49710
|
+
const folder = folderById.get(note.folder ?? -1);
|
|
49711
|
+
return {
|
|
49712
|
+
noteId: noteIdFor(context.uuid, pk),
|
|
49713
|
+
noteIdentifier: note.identifier,
|
|
49714
|
+
noteTitle: note.title,
|
|
49715
|
+
noteModified: coreDataToIso(note.modified),
|
|
49716
|
+
folder: folder?.name ?? null,
|
|
49717
|
+
folderPath: folder ? paths.get(folder.pk) : null,
|
|
49718
|
+
account: account?.name ?? null,
|
|
49719
|
+
accountIdentifier: account?.identifier ?? null
|
|
49720
|
+
};
|
|
49721
|
+
};
|
|
49722
|
+
const containerDir = dirname5(dbPath2);
|
|
49723
|
+
const previewEntries = /* @__PURE__ */ new Map();
|
|
49724
|
+
const previewFor = (identifier, account) => {
|
|
49725
|
+
const accountDir = identifier ? resolveAccountDir(containerDir, account) : null;
|
|
49726
|
+
if (!accountDir || !identifier) return null;
|
|
49727
|
+
if (!previewEntries.has(accountDir))
|
|
49728
|
+
previewEntries.set(accountDir, listPreviewEntries(accountDir));
|
|
49729
|
+
return previewPaths(accountDir, identifier, previewEntries.get(accountDir))[0] ?? null;
|
|
49730
|
+
};
|
|
49731
|
+
const all = [];
|
|
49732
|
+
const add = (pk, link) => all.push({ pk, link: { ...link, ...source(pk) } });
|
|
49733
|
+
for (const [pk, doc] of docs) for (const link of inlineLinks(doc)) add(pk, link);
|
|
49734
|
+
for (const row of rows) {
|
|
49735
|
+
if (row.k === "card") {
|
|
49736
|
+
const noteId3 = noteIdFor(context.uuid, row.note);
|
|
49737
|
+
const link = cardLink(
|
|
49738
|
+
{ pk: row.pk, identifier: row.identifier ?? "", url: row.url, title: row.title },
|
|
49739
|
+
{
|
|
49740
|
+
doc: docs.get(row.note),
|
|
49741
|
+
noteId: noteId3,
|
|
49742
|
+
previewPath: previewFor(row.identifier, source(row.note).accountIdentifier)
|
|
49743
|
+
}
|
|
49744
|
+
);
|
|
49745
|
+
if (link) add(row.note, link);
|
|
49746
|
+
} else if (row.k === "chip") {
|
|
49747
|
+
const link = nativeLink(
|
|
49748
|
+
{ identifier: row.identifier ?? "", token: row.token, alt: row.alt },
|
|
49749
|
+
docs.get(row.note)
|
|
49750
|
+
);
|
|
49751
|
+
if (link) add(row.note, link);
|
|
49752
|
+
}
|
|
49753
|
+
}
|
|
49754
|
+
const wanted = options.kinds?.length ? new Set(options.kinds) : void 0;
|
|
49755
|
+
const links = all.filter(({ link }) => !wanted || wanted.has(link.kind)).sort(
|
|
49756
|
+
(a, b) => (b.link.noteModified ?? "").localeCompare(a.link.noteModified ?? "") || a.pk - b.pk || (a.link.start ?? Number.MAX_SAFE_INTEGER) - (b.link.start ?? Number.MAX_SAFE_INTEGER) || KIND_ORDER2[a.link.kind] - KIND_ORDER2[b.link.kind]
|
|
49757
|
+
).map(({ link }) => link);
|
|
49758
|
+
const counts = { inline: 0, card: 0, note: 0, section: 0 };
|
|
49759
|
+
for (const link of links) counts[link.kind]++;
|
|
49760
|
+
const start = Math.min(Math.max(0, offset), links.length);
|
|
49761
|
+
const page = [];
|
|
49762
|
+
let bytes = 0;
|
|
49763
|
+
for (let i = start; i < links.length && page.length < limit; i++) {
|
|
49764
|
+
const size = Buffer.byteLength(JSON.stringify(links[i]));
|
|
49765
|
+
if (page.length && bytes + size > maxBytes) break;
|
|
49766
|
+
page.push(links[i]);
|
|
49767
|
+
bytes += size;
|
|
49768
|
+
}
|
|
49769
|
+
const next = start + page.length;
|
|
49770
|
+
return {
|
|
49771
|
+
scope: scope2,
|
|
49772
|
+
inlineIncluded: includeInline,
|
|
49773
|
+
notesInScope: notes.length,
|
|
49774
|
+
notesWithoutBody: includeInline ? notes.length - docs.size : 0,
|
|
49775
|
+
counts,
|
|
49776
|
+
links: page,
|
|
49777
|
+
page: {
|
|
49778
|
+
offset: start,
|
|
49779
|
+
returned: page.length,
|
|
49780
|
+
total: links.length,
|
|
49781
|
+
hasMore: next < links.length,
|
|
49782
|
+
...next < links.length ? { nextOffset: next } : {}
|
|
49783
|
+
}
|
|
49784
|
+
};
|
|
49785
|
+
}
|
|
49786
|
+
function describeLinkInventory(result) {
|
|
49787
|
+
const { counts, page } = result;
|
|
49788
|
+
return `Found ${page.total} links in ${result.notesInScope} notes (inline ${counts.inline}${result.inlineIncluded ? "" : " not scanned"}, card ${counts.card}, note ${counts.note}, section ${counts.section}); returned ${page.returned} from offset ${page.offset}` + (page.hasMore ? `; more at offset ${page.nextOffset}` : "") + ".";
|
|
49789
|
+
}
|
|
49790
|
+
|
|
49585
49791
|
// src/utils/linkInsert.ts
|
|
49586
49792
|
var MAX_LINK_URL_LENGTH = 4096;
|
|
49587
49793
|
var MAX_LINK_LABEL_LENGTH = 2e3;
|
|
@@ -49699,7 +49905,7 @@ function insertLink(request, deps) {
|
|
|
49699
49905
|
|
|
49700
49906
|
// src/services/notesExport.ts
|
|
49701
49907
|
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
49702
|
-
import { basename as basename4, dirname as
|
|
49908
|
+
import { basename as basename4, dirname as dirname6, extname as extname4, join as join21 } from "node:path";
|
|
49703
49909
|
|
|
49704
49910
|
// src/utils/exportAssets.ts
|
|
49705
49911
|
import {
|
|
@@ -51022,7 +51228,7 @@ function loadNotes(ids, single, read) {
|
|
|
51022
51228
|
return { notes, skipped };
|
|
51023
51229
|
}
|
|
51024
51230
|
function openOutput(output) {
|
|
51025
|
-
mkdirSync5(
|
|
51231
|
+
mkdirSync5(dirname6(output), { recursive: true });
|
|
51026
51232
|
try {
|
|
51027
51233
|
return openCreateOnly(output);
|
|
51028
51234
|
} catch (error2) {
|
|
@@ -51047,7 +51253,7 @@ function exportNotesMarkdown(request, deps) {
|
|
|
51047
51253
|
const ids = selectNotes(request, deps);
|
|
51048
51254
|
const { notes, skipped } = loadNotes(ids, !!request.id, deps.readNote);
|
|
51049
51255
|
const fd = output ? openOutput(output) : void 0;
|
|
51050
|
-
const writer = assetsDir ? new SidecarWriter(assetsDir, output ?
|
|
51256
|
+
const writer = assetsDir ? new SidecarWriter(assetsDir, output ? dirname6(output) : void 0) : void 0;
|
|
51051
51257
|
const ctx = {
|
|
51052
51258
|
stats: emptyStats(),
|
|
51053
51259
|
...writer ? { writer, locator: deps.locator ?? new AssetLocator() } : {}
|
|
@@ -51077,7 +51283,7 @@ function exportNotesMarkdown(request, deps) {
|
|
|
51077
51283
|
return { ...receipt, markdown };
|
|
51078
51284
|
}
|
|
51079
51285
|
function defaultSidecarDir(output) {
|
|
51080
|
-
return join21(
|
|
51286
|
+
return join21(dirname6(output), `${basename4(output, extname4(output))}.assets`);
|
|
51081
51287
|
}
|
|
51082
51288
|
function exportNotesHtml(request, deps) {
|
|
51083
51289
|
if (!request.outputPath)
|
|
@@ -51098,7 +51304,7 @@ function exportNotesHtml(request, deps) {
|
|
|
51098
51304
|
const ids = selectNotes(request, deps);
|
|
51099
51305
|
const { notes, skipped } = loadNotes(ids, !!request.id, deps.readNote);
|
|
51100
51306
|
const fd = openOutput(output);
|
|
51101
|
-
const writer = assetsDir ? new SidecarWriter(assetsDir,
|
|
51307
|
+
const writer = assetsDir ? new SidecarWriter(assetsDir, dirname6(output)) : new DataUrlWriter();
|
|
51102
51308
|
const ctx = {
|
|
51103
51309
|
stats: emptyStats(),
|
|
51104
51310
|
writer,
|
|
@@ -52204,7 +52410,7 @@ function registerNativeOperations(server2, manager) {
|
|
|
52204
52410
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
52205
52411
|
import { existsSync as existsSync13 } from "node:fs";
|
|
52206
52412
|
import { release as release2 } from "node:os";
|
|
52207
|
-
import { dirname as
|
|
52413
|
+
import { dirname as dirname7, resolve as resolve4 } from "node:path";
|
|
52208
52414
|
import { fileURLToPath } from "node:url";
|
|
52209
52415
|
var OPTIONAL_BRIDGE_NOTE = "(optional \u2014 needed only for create-note format: markdown, macOS 26+)";
|
|
52210
52416
|
var MARKDOWN_MIN_DARWIN_MAJOR = 25;
|
|
@@ -52227,7 +52433,7 @@ function setupShortcuts(checkOnly, dependencies = {}) {
|
|
|
52227
52433
|
const result = spawnSync2("/usr/bin/open", [path10], { encoding: "utf8" });
|
|
52228
52434
|
return result.status === 0 ? { ok: true } : { ok: false, error: result.stderr || result.error?.message || "open failed" };
|
|
52229
52435
|
});
|
|
52230
|
-
const baseDirectory = dependencies.baseDirectory || resolve4(
|
|
52436
|
+
const baseDirectory = dependencies.baseDirectory || resolve4(dirname7(fileURLToPath(import.meta.url)), "../shortcuts");
|
|
52231
52437
|
const osRelease = (dependencies.osRelease || release2)();
|
|
52232
52438
|
const darwinMajor = Number.parseInt(osRelease.split(".")[0], 10);
|
|
52233
52439
|
const items = shortcutFiles.map(({ name, file, optional: optional2 }) => {
|
|
@@ -52308,7 +52514,7 @@ import { spawnSync as spawnSync3 } from "node:child_process";
|
|
|
52308
52514
|
import { createHash as createHash4 } from "node:crypto";
|
|
52309
52515
|
import { existsSync as existsSync14, readFileSync as readFileSync4 } from "node:fs";
|
|
52310
52516
|
import { homedir as homedir19 } from "node:os";
|
|
52311
|
-
import { dirname as
|
|
52517
|
+
import { dirname as dirname8, join as join24, resolve as resolve5 } from "node:path";
|
|
52312
52518
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
52313
52519
|
var PRIVATE_HELPER_PROTOCOL = 1;
|
|
52314
52520
|
var ENABLE_ENV = "APPLE_NOTES_MCP_ENABLE_PRIVATE";
|
|
@@ -52333,7 +52539,7 @@ var manifestSchema = external_exports.object({
|
|
|
52333
52539
|
osVersion: external_exports.string(),
|
|
52334
52540
|
compiler: external_exports.string()
|
|
52335
52541
|
});
|
|
52336
|
-
function packageRoot(fromDir =
|
|
52542
|
+
function packageRoot(fromDir = dirname8(fileURLToPath2(import.meta.url))) {
|
|
52337
52543
|
let dir = fromDir;
|
|
52338
52544
|
for (; ; ) {
|
|
52339
52545
|
const candidate = join24(dir, "package.json");
|
|
@@ -52344,7 +52550,7 @@ function packageRoot(fromDir = dirname7(fileURLToPath2(import.meta.url))) {
|
|
|
52344
52550
|
} catch {
|
|
52345
52551
|
}
|
|
52346
52552
|
}
|
|
52347
|
-
const parent =
|
|
52553
|
+
const parent = dirname8(dir);
|
|
52348
52554
|
if (parent === dir) return resolve5(fromDir, "..");
|
|
52349
52555
|
dir = parent;
|
|
52350
52556
|
}
|
|
@@ -54089,6 +54295,53 @@ registerTool(
|
|
|
54089
54295
|
return successResponse(describeNoteStructure(structure), { ...structure });
|
|
54090
54296
|
}, "Error reading note structure")
|
|
54091
54297
|
);
|
|
54298
|
+
registerTool(
|
|
54299
|
+
"list-note-links",
|
|
54300
|
+
{
|
|
54301
|
+
description: "Use when: you need the links in one note (by exact id) or across a folder (with its subfolders by default), an account, or the whole library, with each link's kind: inline (a hyperlink on text), card (a rich link preview), note (a native link chip to another note) or section (a native link chip to a heading or paragraph).\nReturns: one page of links, newest-modified note first, each with its URL, label, linkSafe, target note and paragraph UUIDs for Notes deep links, card previewPath, and its source noteId, note title, folder path (as list-folders prints it) and account; plus per-kind counts and page info (call again with offset set to page.nextOffset while page.hasMore is true).\nDo not use when: you want one note's full structure (get-note-structure) or its formatting (get-note-blocks).\nSafety: read-only; reads the NoteStore database directly and requires Full Disk Access. Inline links need every body in scope decoded, so they are included only with includeInline (default true for id, false for a folder, account or library scan). Recently Deleted is skipped unless the note is requested by id.",
|
|
54302
|
+
inputSchema: {
|
|
54303
|
+
id: noteIdInput.optional().describe("One exact note ID (do not combine with account/folder)"),
|
|
54304
|
+
account: external_exports.string().min(1).max(MAX.ACCOUNT).optional().describe("Account name (exact or unique-prefix match)"),
|
|
54305
|
+
folder: external_exports.string().min(1).max(MAX.FOLDER).optional().describe(
|
|
54306
|
+
"Folder name or path as list-folders prints it, such as Work/Clients (escape a literal slash as \\/)"
|
|
54307
|
+
),
|
|
54308
|
+
includeSubfolders: external_exports.boolean().optional().describe("With folder, also list notes in its subfolders (default true)"),
|
|
54309
|
+
includeInline: external_exports.boolean().optional().describe(
|
|
54310
|
+
"Also decode note bodies for inline hyperlinks (slower). Default true for id, false otherwise"
|
|
54311
|
+
),
|
|
54312
|
+
kinds: external_exports.array(external_exports.enum(["inline", "card", "note", "section"])).max(4).optional().describe("Only these link kinds"),
|
|
54313
|
+
offset: external_exports.number().int().min(0).optional().describe("Index of the first link to return (default 0); use page.nextOffset"),
|
|
54314
|
+
limit: external_exports.number().int().min(1).max(2e3).optional().describe("Maximum links to return (default 200, max 2000)")
|
|
54315
|
+
},
|
|
54316
|
+
outputSchema: {
|
|
54317
|
+
scope: external_exports.record(external_exports.unknown()).optional(),
|
|
54318
|
+
inlineIncluded: external_exports.boolean().optional(),
|
|
54319
|
+
notesInScope: external_exports.number().optional(),
|
|
54320
|
+
notesWithoutBody: external_exports.number().optional(),
|
|
54321
|
+
counts: external_exports.record(external_exports.unknown()).optional(),
|
|
54322
|
+
links: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
|
|
54323
|
+
page: external_exports.record(external_exports.unknown()).optional()
|
|
54324
|
+
},
|
|
54325
|
+
annotations: { readOnlyHint: true }
|
|
54326
|
+
},
|
|
54327
|
+
withErrorHandling(
|
|
54328
|
+
({ id: id2, account, folder, includeSubfolders, includeInline, kinds, offset, limit }) => {
|
|
54329
|
+
const result = listNoteLinks({
|
|
54330
|
+
id: id2,
|
|
54331
|
+
account,
|
|
54332
|
+
folder,
|
|
54333
|
+
includeSubfolders,
|
|
54334
|
+
includeInline,
|
|
54335
|
+
kinds,
|
|
54336
|
+
offset,
|
|
54337
|
+
limit,
|
|
54338
|
+
maxBytes: blocksMaxResponseBytes()
|
|
54339
|
+
});
|
|
54340
|
+
return successResponse(describeLinkInventory(result), { ...result });
|
|
54341
|
+
},
|
|
54342
|
+
"Error listing note links"
|
|
54343
|
+
)
|
|
54344
|
+
);
|
|
54092
54345
|
registerTool(
|
|
54093
54346
|
"list-native-tags",
|
|
54094
54347
|
{
|
package/package.json
CHANGED