apple-notes-mcp 2.9.28 → 2.9.31
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 +22 -2
- package/build/index.js +92 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -198,6 +198,24 @@ The server is a standard MCP server over stdio, so any client that can launch a
|
|
|
198
198
|
|
|
199
199
|
Read/list/get tools also return **structured JSON** (`structuredContent`) alongside the text, so agents can consume results without parsing prose.
|
|
200
200
|
|
|
201
|
+
Some clients (Claude Desktop among them) pass the model only a result's text
|
|
202
|
+
and drop `structuredContent`. So every result that has `structuredContent` also
|
|
203
|
+
ends with one more text block holding the same data as a single JSON line:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
structuredContent: {"id":"x-coredata://…/ICNote/p123","contentHash":"sha256:…","writable":true,…}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
That line carries every token a follow-up call needs: the `contentHash`
|
|
210
|
+
revision token for `expectedContentHash`, new ids, `nativeTags`, `writable`,
|
|
211
|
+
`page.nextOffset`, and an error's `code`, `committed` and `indeterminate`. A
|
|
212
|
+
long value the text above already shows in full, such as the body from
|
|
213
|
+
`get-note-content`, appears as `"[shown in full above]"` instead of twice. If
|
|
214
|
+
the line would still exceed 16 KB, fields larger than 1 KB (usually a list
|
|
215
|
+
already printed above) are left out and named in `_omitted`. Tools whose text
|
|
216
|
+
already is that JSON get no extra block. `structuredContent` itself is
|
|
217
|
+
unchanged.
|
|
218
|
+
|
|
201
219
|
### MCP resources & prompts
|
|
202
220
|
|
|
203
221
|
Resources expose read-only context the client can attach without a tool call:
|
|
@@ -238,7 +256,9 @@ When Full Disk Access is granted, list and read tools also return stable identif
|
|
|
238
256
|
### Error results
|
|
239
257
|
|
|
240
258
|
A failed call returns `isError: true` with the same human-readable text as
|
|
241
|
-
before, plus `structuredContent` carrying a stable machine-readable `code
|
|
259
|
+
before, plus `structuredContent` carrying a stable machine-readable `code`
|
|
260
|
+
(also repeated in the trailing `structuredContent:` text line, for clients
|
|
261
|
+
that drop `structuredContent`).
|
|
242
262
|
Branch on `code`, not on the prose, which may be reworded.
|
|
243
263
|
|
|
244
264
|
| `code` | Meaning |
|
|
@@ -2409,7 +2429,7 @@ listed under the tool itself:
|
|
|
2409
2429
|
|-----------|------|----------|-------------|
|
|
2410
2430
|
| `id` | string | Yes | Exact note ID: x-coredata id, Notes UUID, or numeric key (see [Identifier forms](#identifier-forms)) |
|
|
2411
2431
|
| `expectedContentHash` | string | Yes | `contentHash` (`sha256:…`) from a fresh read of the note |
|
|
2412
|
-
| `scopeText` | string | Yes | Distinctive phrase already in the note, 12–500 characters, that Notes search can find. Prefer plain words without punctuation, hashtags, or paths |
|
|
2432
|
+
| `scopeText` | string | Yes | Distinctive phrase already in the note below its title line, 12–500 characters, that Notes search can find. Prefer plain words without punctuation, hashtags, or paths |
|
|
2413
2433
|
|
|
2414
2434
|
When the Shortcut refuses a request (its Find Notes step did not return
|
|
2415
2435
|
exactly one note with that exact title and `scopeText`) or stops with its own
|
package/build/index.js
CHANGED
|
@@ -48723,6 +48723,12 @@ function describeTransportFailure(error2) {
|
|
|
48723
48723
|
if (detail?.refused) return `${named} ${detail.message}`;
|
|
48724
48724
|
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)}`;
|
|
48725
48725
|
}
|
|
48726
|
+
function textBelowTitle(text2, title) {
|
|
48727
|
+
const lineEnd = text2.indexOf("\n");
|
|
48728
|
+
const firstLine = lineEnd < 0 ? text2 : text2.slice(0, lineEnd);
|
|
48729
|
+
if (firstLine.trim() !== title.trim()) return text2;
|
|
48730
|
+
return lineEnd < 0 ? "" : text2.slice(lineEnd + 1);
|
|
48731
|
+
}
|
|
48726
48732
|
function mutateBackground(request, operation, data, verify, deps) {
|
|
48727
48733
|
if (request.scopeText.length < 12 || request.scopeText.length > 500 || /[\r\n\0]/u.test(request.scopeText))
|
|
48728
48734
|
throw new Error("Use a distinctive existing single-line scope of 12\u2013500 characters");
|
|
@@ -48731,6 +48737,11 @@ function mutateBackground(request, operation, data, verify, deps) {
|
|
|
48731
48737
|
throw new Error("Note revision changed; read it again");
|
|
48732
48738
|
if (!before.rich.text.includes(request.scopeText))
|
|
48733
48739
|
throw new Error("Scope is absent from exact note");
|
|
48740
|
+
if (!textBelowTitle(before.rich.text, before.title).includes(request.scopeText))
|
|
48741
|
+
throw new CodedError(
|
|
48742
|
+
"scopeText appears only in the note's title line. The bridge's Find Notes step matches it against the body below the title, so it would refuse; pass a phrase from below the title. Nothing changed",
|
|
48743
|
+
{ code: "validation_error", committed: false, indeterminate: false }
|
|
48744
|
+
);
|
|
48734
48745
|
const candidates = deps.candidates(before.title, request.scopeText);
|
|
48735
48746
|
if (candidates.length !== 1 || candidates[0] !== request.id)
|
|
48736
48747
|
throw new Error("Ambiguous note selection; nothing changed");
|
|
@@ -53475,6 +53486,85 @@ function exportWithTemplate(request, deps, chosen, notes, skipped, { output, ass
|
|
|
53475
53486
|
return { ...receipt, markdown };
|
|
53476
53487
|
}
|
|
53477
53488
|
|
|
53489
|
+
// src/utils/structuredText.ts
|
|
53490
|
+
import { isDeepStrictEqual } from "node:util";
|
|
53491
|
+
var STRUCTURED_TEXT_PREFIX = "structuredContent: ";
|
|
53492
|
+
var SHOWN_ABOVE = "[shown in full above]";
|
|
53493
|
+
var MIN_ELIDED_CHARS = 64;
|
|
53494
|
+
var MAX_MIRROR_CHARS = 16384;
|
|
53495
|
+
var MAX_FIELD_CHARS = 1024;
|
|
53496
|
+
function textOf(content) {
|
|
53497
|
+
return content.map(
|
|
53498
|
+
(item) => item && typeof item === "object" && item.type === "text" ? String(item.text ?? "") : ""
|
|
53499
|
+
).join("\n");
|
|
53500
|
+
}
|
|
53501
|
+
function elide(value, text2) {
|
|
53502
|
+
if (typeof value === "string") {
|
|
53503
|
+
return value.length >= MIN_ELIDED_CHARS && text2.includes(value) ? SHOWN_ABOVE : value;
|
|
53504
|
+
}
|
|
53505
|
+
if (Array.isArray(value)) return value.map((item) => elide(item, text2));
|
|
53506
|
+
if (value && typeof value === "object") {
|
|
53507
|
+
const out = {};
|
|
53508
|
+
for (const [key, item] of Object.entries(value)) {
|
|
53509
|
+
if (item !== void 0) out[key] = elide(item, text2);
|
|
53510
|
+
}
|
|
53511
|
+
return out;
|
|
53512
|
+
}
|
|
53513
|
+
return value;
|
|
53514
|
+
}
|
|
53515
|
+
function textIsTheJson(content, structured) {
|
|
53516
|
+
const plain = JSON.parse(JSON.stringify(structured));
|
|
53517
|
+
return content.some((item) => {
|
|
53518
|
+
const text2 = item?.text;
|
|
53519
|
+
if (item?.type !== "text" || typeof text2 !== "string") return false;
|
|
53520
|
+
const trimmed = text2.trim();
|
|
53521
|
+
if (!trimmed.startsWith("{")) return false;
|
|
53522
|
+
try {
|
|
53523
|
+
return isDeepStrictEqual(JSON.parse(trimmed), plain);
|
|
53524
|
+
} catch {
|
|
53525
|
+
return false;
|
|
53526
|
+
}
|
|
53527
|
+
});
|
|
53528
|
+
}
|
|
53529
|
+
function structuredTextLine(structured, text2) {
|
|
53530
|
+
const elided = elide(structured, text2);
|
|
53531
|
+
let json2 = JSON.stringify(elided);
|
|
53532
|
+
if (json2.length > MAX_MIRROR_CHARS) {
|
|
53533
|
+
const kept = {};
|
|
53534
|
+
const omitted = [];
|
|
53535
|
+
for (const [key, value] of Object.entries(elided)) {
|
|
53536
|
+
if (JSON.stringify(value).length > MAX_FIELD_CHARS) omitted.push(key);
|
|
53537
|
+
else kept[key] = value;
|
|
53538
|
+
}
|
|
53539
|
+
json2 = JSON.stringify({ ...kept, _omitted: omitted });
|
|
53540
|
+
}
|
|
53541
|
+
return STRUCTURED_TEXT_PREFIX + json2;
|
|
53542
|
+
}
|
|
53543
|
+
function withStructuredText(result) {
|
|
53544
|
+
const r = result;
|
|
53545
|
+
if (!r || typeof r !== "object") return result;
|
|
53546
|
+
const structured = r.structuredContent;
|
|
53547
|
+
if (!structured || typeof structured !== "object" || Array.isArray(structured)) return result;
|
|
53548
|
+
const content = Array.isArray(r.content) ? r.content : [];
|
|
53549
|
+
if (textIsTheJson(content, structured)) return result;
|
|
53550
|
+
const line = structuredTextLine(structured, textOf(content));
|
|
53551
|
+
return { ...r, content: [...content, { type: "text", text: line }] };
|
|
53552
|
+
}
|
|
53553
|
+
function installStructuredText(server2) {
|
|
53554
|
+
const target = server2;
|
|
53555
|
+
const register = target.registerTool;
|
|
53556
|
+
if (typeof register === "function") {
|
|
53557
|
+
target.registerTool = function(name, config2, cb) {
|
|
53558
|
+
const wrapped = typeof cb === "function" ? async (...args) => withStructuredText(await cb(...args)) : cb;
|
|
53559
|
+
return register.call(this, name, config2, wrapped);
|
|
53560
|
+
};
|
|
53561
|
+
}
|
|
53562
|
+
const createError = target.createToolError;
|
|
53563
|
+
if (typeof createError === "function") {
|
|
53564
|
+
target.createToolError = (message) => withStructuredText(createError.call(target, message));
|
|
53565
|
+
}
|
|
53566
|
+
}
|
|
53567
|
+
|
|
53478
53568
|
// src/tools/directOperations.ts
|
|
53479
53569
|
import { createHash as createHash3 } from "node:crypto";
|
|
53480
53570
|
import {
|
|
@@ -57108,7 +57198,7 @@ var common = {
|
|
|
57108
57198
|
id,
|
|
57109
57199
|
expectedContentHash: revision3,
|
|
57110
57200
|
scopeText: external_exports.string().min(12).max(500).describe(
|
|
57111
|
-
"Distinctive existing phrase used by Notes search. Prefer plain words without punctuation, hashtags, or paths."
|
|
57201
|
+
"Distinctive existing phrase from below the title line, used by Notes search. Prefer plain words without punctuation, hashtags, or paths."
|
|
57112
57202
|
)
|
|
57113
57203
|
};
|
|
57114
57204
|
var EMPTY_TABLE_ROWS = [
|
|
@@ -59292,6 +59382,7 @@ var server = new McpServer({
|
|
|
59292
59382
|
description: "MCP server for managing Apple Notes - create, search, update, and organize notes"
|
|
59293
59383
|
});
|
|
59294
59384
|
installSdkErrorCodes(server);
|
|
59385
|
+
installStructuredText(server);
|
|
59295
59386
|
var notesManager = new AppleNotesManager();
|
|
59296
59387
|
registerDirectOperations(server, notesManager);
|
|
59297
59388
|
registerFolderDelete(server, notesManager);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-notes-mcp",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.31",
|
|
4
4
|
"description": "MCP server for Apple Notes - read, search, create, edit, organize, and export notes on macOS via Claude and other AI assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|