@mgcrea/mcp-apple-notes 0.0.0-bootstrap
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/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +28 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +433 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/server-HLS_dM7r.js +1550 -0
- package/dist/server-HLS_dM7r.js.map +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Olivier Louvignes <olivier@mgcrea.io>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @mgcrea/mcp-apple-notes
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for the macOS **Apple Notes** app.
|
|
4
|
+
|
|
5
|
+
> **Unofficial.** Not affiliated with Apple. It drives the Notes app that is already on your Mac.
|
|
6
|
+
|
|
7
|
+
## Two lanes
|
|
8
|
+
|
|
9
|
+
- **Apple Events lane** — accounts, folders, bodies, and every mutation. Always the authority:
|
|
10
|
+
after a write, the result is what Notes re-read.
|
|
11
|
+
- **Index lane** — read-only SQLite over `NoteStore.sqlite`, for search and metadata at a scale
|
|
12
|
+
Apple Events cannot reach. Needs Full Disk Access.
|
|
13
|
+
|
|
14
|
+
Unlike Mail, **the Apple Events lane alone is a working server**. Measured on a 921-note library, a
|
|
15
|
+
full-text search takes 97 ms — against the 74 s that forced Mail onto its index. So you can install
|
|
16
|
+
this, use it, and only then decide whether to grant Full Disk Access.
|
|
17
|
+
|
|
18
|
+
It does not scale for ever: there is no index behind it, so every query pays the full cost again.
|
|
19
|
+
The wall is around 5–10k notes. `apple_notes_diagnostics` tells you which lane is answering.
|
|
20
|
+
|
|
21
|
+
| Permission | Needed for | Without it |
|
|
22
|
+
| ---------------------- | --------------------------------- | ----------------------------------------------- |
|
|
23
|
+
| **Automation → Notes** | everything | nothing works; you get a `-1743` error |
|
|
24
|
+
| **Full Disk Access** | search at scale, attachment bytes | the server still runs, on the Apple Events lane |
|
|
25
|
+
|
|
26
|
+
Neither is granted to Notes.app — it is the _reader_ that needs permission.
|
|
27
|
+
|
|
28
|
+
## Tools
|
|
29
|
+
|
|
30
|
+
Read: `diagnostics`, `list_accounts`, `list_folders`, `list_notes`, `search_notes`, `get_note`,
|
|
31
|
+
`list_attachments`, `save_attachment`.
|
|
32
|
+
|
|
33
|
+
Write, registered **only** when `APPLE_NOTES_ALLOW_WRITES=1` — with the flag off they are invisible
|
|
34
|
+
to the model, not merely refused: `create_note`, `update_note`, `move_note`, `delete_notes`.
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
| Variable | Default | |
|
|
39
|
+
| ---------------------------------- | ------------- | ------------------------------------------------- |
|
|
40
|
+
| `APPLE_NOTES_ALLOW_WRITES` | off | Register the mutating tools. |
|
|
41
|
+
| `APPLE_NOTES_ACCOUNTS` | all | Read-side allowlist, comma-separated. |
|
|
42
|
+
| `APPLE_NOTES_INDEX_MODE` | `auto` | `auto` \| `ro` \| `immutable` \| `off`. |
|
|
43
|
+
| `APPLE_NOTES_STORE` | auto | Explicit `NoteStore.sqlite` path. |
|
|
44
|
+
| `APPLE_NOTES_SEARCH_CACHE_TTL_MS` | `60000` | How long the body corpus stays warm. |
|
|
45
|
+
| `APPLE_NOTES_ATTACHMENT_DIR` | `~/Downloads` | The only directory attachments may be saved into. |
|
|
46
|
+
| `APPLE_NOTES_BODY_MAX_BYTES` | `262144` | Body truncation, to protect the context window. |
|
|
47
|
+
| `APPLE_NOTES_OSASCRIPT_TIMEOUT_MS` | `30000` | Sized for the first-run permission prompt. |
|
|
48
|
+
|
|
49
|
+
## Notes that will bite you
|
|
50
|
+
|
|
51
|
+
- **Notes takes a note's title from the first line of its body.** Setting `name` is silently
|
|
52
|
+
ignored, so `create_note` prepends the title as a heading instead.
|
|
53
|
+
- **Password-protected notes cannot be read at all.** Their text is AES ciphertext at rest, which
|
|
54
|
+
is why `ZICNOTEDATA` carries crypto columns; no permission makes it readable.
|
|
55
|
+
- **Folders nest.** A folder map has to recurse even if your own library happens to be flat.
|
|
56
|
+
- **Search caches on a TTL, not on invalidation.** Checking whether the cache is stale costs _more_
|
|
57
|
+
over Apple Events (128 ms) than redoing the scan (97 ms), so bounded staleness is the honest
|
|
58
|
+
trade. See [docs/notes.md](../../docs/notes.md).
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { D as BUILD_INFO, S as NOTES_SURFACE, a as loadConfig, r as createServer } from "./server-HLS_dM7r.js";
|
|
3
|
+
import { runStdioServer } from "@mgcrea/mcp-apple-core";
|
|
4
|
+
//#region src/cli.ts
|
|
5
|
+
const LOG_PREFIX = "apple-notes-mcp";
|
|
6
|
+
runStdioServer({
|
|
7
|
+
build: BUILD_INFO,
|
|
8
|
+
surface: NOTES_SURFACE,
|
|
9
|
+
logPrefix: LOG_PREFIX,
|
|
10
|
+
start: async (logger) => {
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
const { server } = createServer({
|
|
13
|
+
config,
|
|
14
|
+
logger
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
server,
|
|
18
|
+
banner: `writes=${config.allowWrites ? "ENABLED" : "disabled"}, accounts=${config.accounts.length ? config.accounts.join("/") : "all"}, index=${config.indexMode}`
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}).catch((err) => {
|
|
22
|
+
console.error(`[${LOG_PREFIX}] fatal:`, err);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
//#endregion
|
|
26
|
+
export {};
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { runStdioServer } from \"@mgcrea/mcp-apple-core\";\n\nimport { BUILD_INFO } from \"./build-info.js\";\nimport { NOTES_SURFACE } from \"./client/errors.js\";\nimport { loadConfig } from \"./config.js\";\nimport { createServer } from \"./server.js\";\n\nconst LOG_PREFIX = \"apple-notes-mcp\";\n\nrunStdioServer({\n build: BUILD_INFO,\n surface: NOTES_SURFACE,\n logPrefix: LOG_PREFIX,\n start: async (logger) => {\n const config = loadConfig();\n const { server } = createServer({ config, logger });\n return {\n server,\n banner:\n `writes=${config.allowWrites ? \"ENABLED\" : \"disabled\"}, ` +\n `accounts=${config.accounts.length ? config.accounts.join(\"/\") : \"all\"}, ` +\n `index=${config.indexMode}`,\n };\n },\n}).catch((err: unknown) => {\n console.error(`[${LOG_PREFIX}] fatal:`, err);\n process.exit(1);\n});\n"],"mappings":";;;;AAQA,MAAM,aAAa;AAEnB,eAAe;CACb,OAAO;CACP,SAAS;CACT,WAAW;CACX,OAAO,OAAO,WAAW;EACvB,MAAM,SAAS,WAAW;EAC1B,MAAM,EAAE,WAAW,aAAa;GAAE;GAAQ;EAAO,CAAC;EAClD,OAAO;GACL;GACA,QACE,UAAU,OAAO,cAAc,YAAY,WAAW,aAC1C,OAAO,SAAS,SAAS,OAAO,SAAS,KAAK,GAAG,IAAI,MAAM,UAC9D,OAAO;EACpB;CACF;AACF,CAAC,CAAC,CAAC,OAAO,QAAiB;CACzB,QAAQ,MAAM,IAAI,WAAW,WAAW,GAAG;CAC3C,QAAQ,KAAK,CAAC;AAChB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { AppBusyError as NotesBusyError, AppNotRunningError as NotesNotRunningError, AppleAutomationError, AppleAutomationError as AppleNotesError, BuildInfo, Logger, OsascriptRunner, ReadOnlyMode, StoreFacts, SurfaceContext } from "@mgcrea/mcp-apple-core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
//#region src/build-info.d.ts
|
|
6
|
+
declare const BUILD_INFO: BuildInfo;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/client/errors.d.ts
|
|
9
|
+
/** Named in every user-facing error and in the env vars they mention. */
|
|
10
|
+
declare const NOTES_SURFACE: SurfaceContext;
|
|
11
|
+
/** A NoteRef no longer resolves — the note was deleted, or the index is stale. */
|
|
12
|
+
declare class NoteNotFoundError extends AppleAutomationError {
|
|
13
|
+
readonly name = "NoteNotFoundError";
|
|
14
|
+
constructor(ref: string);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The note is password-protected and Notes will not hand over its text.
|
|
18
|
+
*
|
|
19
|
+
* The file lane cannot help either: locked notes are AES ciphertext at rest,
|
|
20
|
+
* which is why `ZICNOTEDATA` carries `ZCRYPTOINITIALIZATIONVECTOR` and
|
|
21
|
+
* `ZCRYPTOTAG` columns. Saying so is more useful than an empty body.
|
|
22
|
+
*/
|
|
23
|
+
declare class NoteLockedError extends AppleAutomationError {
|
|
24
|
+
readonly name = "NoteLockedError";
|
|
25
|
+
constructor(ref: string);
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/client/locate.d.ts
|
|
29
|
+
type LocateResult = StoreFacts & {
|
|
30
|
+
storePath: string;
|
|
31
|
+
/** Where attachment bytes live, one directory per account. */
|
|
32
|
+
mediaRoot: string;
|
|
33
|
+
reason: string | null;
|
|
34
|
+
};
|
|
35
|
+
declare const defaultStorePath: (home?: string) => string;
|
|
36
|
+
declare const locateStore: (opts?: {
|
|
37
|
+
storePath?: string | undefined;
|
|
38
|
+
home?: string;
|
|
39
|
+
}) => LocateResult;
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/config.d.ts
|
|
42
|
+
/**
|
|
43
|
+
* Configuration is environment-only — this server holds no secret at all, its
|
|
44
|
+
* access is the macOS permission the user granted.
|
|
45
|
+
*
|
|
46
|
+
* `allowWrites`, `debug`, `osascriptPath`, `osascriptTimeoutMs` and `maxResults`
|
|
47
|
+
* come from `BaseConfigSchema`.
|
|
48
|
+
*/
|
|
49
|
+
declare const ConfigSchema: z.ZodObject<{
|
|
50
|
+
allowWrites: z.ZodDefault<z.ZodBoolean>;
|
|
51
|
+
debug: z.ZodDefault<z.ZodBoolean>;
|
|
52
|
+
osascriptPath: z.ZodDefault<z.ZodString>;
|
|
53
|
+
osascriptTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
54
|
+
maxResults: z.ZodDefault<z.ZodNumber>;
|
|
55
|
+
accounts: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
56
|
+
storePath: z.ZodOptional<z.ZodString>;
|
|
57
|
+
indexMode: z.ZodDefault<z.ZodEnum<{
|
|
58
|
+
auto: "auto";
|
|
59
|
+
immutable: "immutable";
|
|
60
|
+
off: "off";
|
|
61
|
+
ro: "ro";
|
|
62
|
+
}>>;
|
|
63
|
+
degradedMaxNotes: z.ZodDefault<z.ZodNumber>;
|
|
64
|
+
bodyMaxBytes: z.ZodDefault<z.ZodNumber>;
|
|
65
|
+
attachmentDir: z.ZodDefault<z.ZodString>;
|
|
66
|
+
searchCacheTtlMs: z.ZodDefault<z.ZodNumber>;
|
|
67
|
+
}, z.core.$strict>;
|
|
68
|
+
type Config = z.infer<typeof ConfigSchema>;
|
|
69
|
+
/**
|
|
70
|
+
* `env` is a parameter with a default so tests are hermetic — they pass their
|
|
71
|
+
* own object rather than mutating (and having to restore) process.env.
|
|
72
|
+
*/
|
|
73
|
+
declare const loadConfig: (env?: NodeJS.ProcessEnv) => Config;
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/client/store.d.ts
|
|
76
|
+
type NoteRow = {
|
|
77
|
+
primaryKey: number;
|
|
78
|
+
title: string | null;
|
|
79
|
+
snippet: string | null;
|
|
80
|
+
modified: string | null;
|
|
81
|
+
created: string | null;
|
|
82
|
+
folderPk: number | null;
|
|
83
|
+
locked: boolean;
|
|
84
|
+
};
|
|
85
|
+
type StoreCapabilities = {
|
|
86
|
+
fingerprint: string;
|
|
87
|
+
/**
|
|
88
|
+
* The persistent store UUID, which is the middle segment of every note's
|
|
89
|
+
* Apple Events id (`x-coredata://<uuid>/ICNote/p<N>`). Reading it here lets an
|
|
90
|
+
* index row be turned into a ref without asking Notes anything.
|
|
91
|
+
*/
|
|
92
|
+
storeUuid: string | null;
|
|
93
|
+
/** `Z_ENT` for `ICNote`, looked up by name rather than hardcoded to 12. */
|
|
94
|
+
noteEnt: number | null;
|
|
95
|
+
/** Present on this schema, so the queries can use them. */
|
|
96
|
+
has: {
|
|
97
|
+
title: boolean;
|
|
98
|
+
snippet: boolean;
|
|
99
|
+
widgetSnippet: boolean;
|
|
100
|
+
folder: boolean;
|
|
101
|
+
locked: boolean;
|
|
102
|
+
};
|
|
103
|
+
deletionColumns: string[];
|
|
104
|
+
missing: string[];
|
|
105
|
+
};
|
|
106
|
+
declare const introspect: (db: DatabaseSync) => StoreCapabilities;
|
|
107
|
+
type SearchFilters = {
|
|
108
|
+
query?: string | undefined;
|
|
109
|
+
folderPk?: number | undefined;
|
|
110
|
+
modifiedAfter?: string | undefined;
|
|
111
|
+
modifiedBefore?: string | undefined;
|
|
112
|
+
limit: number;
|
|
113
|
+
offset: number;
|
|
114
|
+
};
|
|
115
|
+
declare class NoteStore {
|
|
116
|
+
#private;
|
|
117
|
+
readonly caps: StoreCapabilities;
|
|
118
|
+
readonly mode: "ro" | "immutable";
|
|
119
|
+
constructor(db: DatabaseSync, caps: StoreCapabilities, mode: "ro" | "immutable");
|
|
120
|
+
count(): number;
|
|
121
|
+
/**
|
|
122
|
+
* Title and snippet search.
|
|
123
|
+
*
|
|
124
|
+
* Deliberately not full text: no indexed column carries the body. Callers that
|
|
125
|
+
* need body matching combine this with `bodyOf` or fall back to the Apple
|
|
126
|
+
* Events bulk scan.
|
|
127
|
+
*/
|
|
128
|
+
search(filters: SearchFilters): NoteRow[];
|
|
129
|
+
byPrimaryKey(pk: number): NoteRow | null;
|
|
130
|
+
/**
|
|
131
|
+
* The note body, decoded from the gzipped protobuf.
|
|
132
|
+
*
|
|
133
|
+
* Returns null for password-protected notes: their `ZDATA` is AES ciphertext,
|
|
134
|
+
* flagged by a non-null `ZCRYPTOTAG`. No permission makes that readable.
|
|
135
|
+
*/
|
|
136
|
+
bodyOf(pk: number): {
|
|
137
|
+
text: string | null;
|
|
138
|
+
via: string | null;
|
|
139
|
+
encrypted: boolean;
|
|
140
|
+
};
|
|
141
|
+
close(): void;
|
|
142
|
+
}
|
|
143
|
+
declare const openStore: (path: string, mode: ReadOnlyMode, logger?: Logger) => NoteStore;
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/client/notes.d.ts
|
|
146
|
+
type LaneStatus = {
|
|
147
|
+
applescript: "live" | "unavailable";
|
|
148
|
+
index: "live" | "unavailable" | "disabled";
|
|
149
|
+
indexMode: string | null;
|
|
150
|
+
storeFingerprint: string | null;
|
|
151
|
+
reason: string | null;
|
|
152
|
+
};
|
|
153
|
+
type NoteAccount = {
|
|
154
|
+
id: string | null;
|
|
155
|
+
name: string | null;
|
|
156
|
+
defaultFolder: string | null;
|
|
157
|
+
folderCount: number;
|
|
158
|
+
noteCount: number;
|
|
159
|
+
};
|
|
160
|
+
type NoteFolder = {
|
|
161
|
+
id: string | null;
|
|
162
|
+
name: string | null;
|
|
163
|
+
accountId: string | null;
|
|
164
|
+
accountName: string | null;
|
|
165
|
+
depth: number;
|
|
166
|
+
shared: boolean;
|
|
167
|
+
noteCount: number | null;
|
|
168
|
+
};
|
|
169
|
+
type NoteSummary = {
|
|
170
|
+
ref: string;
|
|
171
|
+
title: string | null;
|
|
172
|
+
snippet: string | null;
|
|
173
|
+
folder: string | null;
|
|
174
|
+
account: string | null;
|
|
175
|
+
modified: string | null;
|
|
176
|
+
created: string | null;
|
|
177
|
+
locked: boolean;
|
|
178
|
+
source: "index" | "apple-events";
|
|
179
|
+
};
|
|
180
|
+
type AppleNotesClientOptions = {
|
|
181
|
+
config: Config;
|
|
182
|
+
logger?: Logger | undefined;
|
|
183
|
+
/** Injected by tests so nothing spawns a process or touches a real Notes. */
|
|
184
|
+
osascript?: OsascriptRunner | undefined;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* The Notes client.
|
|
188
|
+
*
|
|
189
|
+
* Two lanes, and which one answers depends on what was granted:
|
|
190
|
+
*
|
|
191
|
+
* - **Apple Events** — accounts, folders, every mutation, and bodies. Always the
|
|
192
|
+
* authority: after a write, the result is what Notes re-read.
|
|
193
|
+
* - **Index** — `NoteStore.sqlite`, for search and metadata at a scale Apple
|
|
194
|
+
* Events cannot reach.
|
|
195
|
+
*
|
|
196
|
+
* The discipline the measurements impose is in `jxa/core.ts`: bulk array fetches
|
|
197
|
+
* only, never `whose`, never a property read per note.
|
|
198
|
+
*/
|
|
199
|
+
declare class AppleNotesClient {
|
|
200
|
+
#private;
|
|
201
|
+
readonly config: Config;
|
|
202
|
+
readonly runner: OsascriptRunner;
|
|
203
|
+
constructor(opts: AppleNotesClientOptions);
|
|
204
|
+
locate(): LocateResult;
|
|
205
|
+
/** Open the index once and remember the outcome, including the failure. */
|
|
206
|
+
index(): NoteStore | null;
|
|
207
|
+
/**
|
|
208
|
+
* Probe both lanes.
|
|
209
|
+
*
|
|
210
|
+
* Apple Events is probed twice with a gap: the first event is what triggers
|
|
211
|
+
* the Automation prompt, and it can return failure while the user is still
|
|
212
|
+
* deciding — reporting "denied" then would be wrong.
|
|
213
|
+
*/
|
|
214
|
+
lanes(): Promise<LaneStatus>;
|
|
215
|
+
accounts(): Promise<NoteAccount[]>;
|
|
216
|
+
folders(): Promise<NoteFolder[]>;
|
|
217
|
+
/**
|
|
218
|
+
* Note metadata.
|
|
219
|
+
*
|
|
220
|
+
* Prefers the index — it holds the same fields, costs no Apple Event, and
|
|
221
|
+
* keeps working as a library grows past the point where a bulk fetch stops
|
|
222
|
+
* being instant.
|
|
223
|
+
*/
|
|
224
|
+
listNotes(opts: {
|
|
225
|
+
folder?: string | undefined;
|
|
226
|
+
limit: number;
|
|
227
|
+
}): Promise<NoteSummary[]>;
|
|
228
|
+
/**
|
|
229
|
+
* Search.
|
|
230
|
+
*
|
|
231
|
+
* `title` uses the index's title and snippet columns, which is a SQL query.
|
|
232
|
+
* `full` needs the body, which no indexed column carries — so it decodes the
|
|
233
|
+
* gzipped protobuf when the index is available and falls back to the Apple
|
|
234
|
+
* Events bulk scan when it is not. Either way the match happens in JS over a
|
|
235
|
+
* cached corpus, because `whose` is 6.9x slower than fetching and filtering.
|
|
236
|
+
*/
|
|
237
|
+
searchNotes(opts: {
|
|
238
|
+
query: string;
|
|
239
|
+
scope: "title" | "full";
|
|
240
|
+
limit: number;
|
|
241
|
+
offset: number;
|
|
242
|
+
}): Promise<{
|
|
243
|
+
notes: NoteSummary[];
|
|
244
|
+
source: "index" | "apple-events";
|
|
245
|
+
scope: string;
|
|
246
|
+
}>;
|
|
247
|
+
getNote(ref: string, opts?: {
|
|
248
|
+
body?: boolean;
|
|
249
|
+
}): Promise<{
|
|
250
|
+
summary: NoteSummary;
|
|
251
|
+
body: string | null;
|
|
252
|
+
bodySource: string | null;
|
|
253
|
+
}>;
|
|
254
|
+
attachments(ref: string): Promise<{
|
|
255
|
+
id: string | null;
|
|
256
|
+
name: string | null;
|
|
257
|
+
url: string | null;
|
|
258
|
+
contentIdentifier: string | null;
|
|
259
|
+
}[]>;
|
|
260
|
+
/**
|
|
261
|
+
* Save an attachment's bytes.
|
|
262
|
+
*
|
|
263
|
+
* The scripting dictionary exposes `name`, `id`, `URL` and `content
|
|
264
|
+
* identifier` but **no filesystem path**, so the bytes can only come from the
|
|
265
|
+
* media directory — which is Full Disk Access territory. Without it this
|
|
266
|
+
* reports what is missing rather than failing obscurely.
|
|
267
|
+
*/
|
|
268
|
+
saveAttachment(ref: string, attachmentId: string, targetDir?: string, opts?: {
|
|
269
|
+
overwrite?: boolean;
|
|
270
|
+
}): Promise<{
|
|
271
|
+
path: string;
|
|
272
|
+
bytes: number;
|
|
273
|
+
}>;
|
|
274
|
+
createNote(opts: {
|
|
275
|
+
title?: string;
|
|
276
|
+
body?: string;
|
|
277
|
+
folderId?: string;
|
|
278
|
+
}): Promise<unknown>;
|
|
279
|
+
updateNote(ref: string, body: string, mode: "replace" | "append"): Promise<unknown>;
|
|
280
|
+
moveNote(ref: string, folderId: string): Promise<unknown>;
|
|
281
|
+
deleteNotes(refs: string[]): Promise<unknown>;
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/client/protobuf.d.ts
|
|
285
|
+
/**
|
|
286
|
+
* Minimal protobuf wire-format reader, enough to pull the body text out of a
|
|
287
|
+
* `ZICNOTEDATA.ZDATA` blob.
|
|
288
|
+
*
|
|
289
|
+
* ## Why it does not hardcode Apple's schema
|
|
290
|
+
*
|
|
291
|
+
* The usual approach compiles Apple's unpublished `.proto` and reads
|
|
292
|
+
* `document.note.note_text` by field number. That breaks the first time Apple
|
|
293
|
+
* renumbers a field, and this project already budgets for the schema drifting
|
|
294
|
+
* on every macOS release.
|
|
295
|
+
*
|
|
296
|
+
* So this walks the wire format structurally, and the path is *measured* rather
|
|
297
|
+
* than assumed — see `NOTE_TEXT_PATH`.
|
|
298
|
+
*
|
|
299
|
+
* Wire format, for reference:
|
|
300
|
+
* tag = varint; field = tag >> 3; wire type = tag & 7
|
|
301
|
+
* 0 varint · 1 fixed64 · 2 length-delimited · 5 fixed32 · 3/4 groups (legacy)
|
|
302
|
+
*
|
|
303
|
+
* Pure and I/O-free: callers gunzip, callers decide what to do with the result.
|
|
304
|
+
*/
|
|
305
|
+
type Varint = {
|
|
306
|
+
value: number;
|
|
307
|
+
next: number;
|
|
308
|
+
};
|
|
309
|
+
/** Read a base-128 varint. Returns null rather than throwing on a truncated one. */
|
|
310
|
+
declare const readVarint: (buf: Uint8Array, pos: number) => Varint | null;
|
|
311
|
+
type WireField = {
|
|
312
|
+
field: number;
|
|
313
|
+
wire: number;
|
|
314
|
+
value?: number;
|
|
315
|
+
from?: number;
|
|
316
|
+
to?: number;
|
|
317
|
+
};
|
|
318
|
+
/**
|
|
319
|
+
* Split a buffer into wire-format fields. Returns null if the bytes are not a
|
|
320
|
+
* well-formed message ending exactly on the boundary — that "exactly" is what
|
|
321
|
+
* makes this usable as a test for whether a blob is a nested message.
|
|
322
|
+
*/
|
|
323
|
+
declare const parseFields: (buf: Uint8Array, start?: number, end?: number) => WireField[] | null;
|
|
324
|
+
type StringCandidate = {
|
|
325
|
+
path: string;
|
|
326
|
+
text: string;
|
|
327
|
+
length: number;
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* Every string in the blob, with the field path that reached it.
|
|
331
|
+
*
|
|
332
|
+
* A length-delimited field can be both a valid message and valid text, so both
|
|
333
|
+
* readings are followed. That ambiguity is the normal case, not an error.
|
|
334
|
+
*/
|
|
335
|
+
declare const collectStrings: (buf: Uint8Array, maxDepth?: number) => StringCandidate[];
|
|
336
|
+
/**
|
|
337
|
+
* Where the body lives: `document(2) -> note(3) -> note_text(2)`.
|
|
338
|
+
*
|
|
339
|
+
* Measured, not assumed. Running the structural walk over 53 real notes and
|
|
340
|
+
* checking each result against the same note's Apple Events plaintext, every
|
|
341
|
+
* note read from this path matched exactly (53/53), and picking the longest
|
|
342
|
+
* string instead matched only 27/53 — on the other 26 an attribute-run field
|
|
343
|
+
* outran the body by ~60 characters. That failure is silent: it returns
|
|
344
|
+
* plausible, wrong text rather than an error.
|
|
345
|
+
*/
|
|
346
|
+
declare const NOTE_TEXT_PATH = "2.3.2";
|
|
347
|
+
type DecodedNote = {
|
|
348
|
+
text: string | null;
|
|
349
|
+
path: string | null;
|
|
350
|
+
/** `pinned` or `longest`. A shift to `longest` is the schema-drift signal. */
|
|
351
|
+
via: "pinned" | "longest" | null;
|
|
352
|
+
candidateCount: number;
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* The note body.
|
|
356
|
+
*
|
|
357
|
+
* Reads the pinned path first and falls back to the longest string only if that
|
|
358
|
+
* path is absent, reporting which route it took so drift is detectable rather
|
|
359
|
+
* than silently wrong.
|
|
360
|
+
*/
|
|
361
|
+
declare const extractNoteText: (buf: Uint8Array, preferPath?: string | null) => DecodedNote;
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/client/ref.d.ts
|
|
364
|
+
/**
|
|
365
|
+
* NoteRef — the one identifier any tool accepts or returns.
|
|
366
|
+
*
|
|
367
|
+
* Notes' own id is already globally unique and resolvable without a container,
|
|
368
|
+
* unlike Mail's message ids: `x-coredata://<store>/ICNote/p<N>` addresses a note
|
|
369
|
+
* outright, so this carries no folder and needs no lookup ladder.
|
|
370
|
+
*
|
|
371
|
+
* Wire format: n1:<coredata-id>
|
|
372
|
+
*
|
|
373
|
+
* The `n1:` prefix is deliberate. If the id scheme ever changes, a versioned
|
|
374
|
+
* prefix makes that an additive change instead of a silent reinterpretation of
|
|
375
|
+
* every ref already in a conversation.
|
|
376
|
+
*
|
|
377
|
+
* Deliberately NOT shared with Mail's `ref.ts`: that codec's `#(\d+)$` and
|
|
378
|
+
* `Number.isSafeInteger` both assume an integer id, which this is not.
|
|
379
|
+
*/
|
|
380
|
+
declare const REF_VERSION = "n1";
|
|
381
|
+
type NoteRef = {
|
|
382
|
+
/** The full Core Data URI, which is what Apple Events resolves. */
|
|
383
|
+
id: string;
|
|
384
|
+
/** `Z_PK` in `ZICCLOUDSYNCINGOBJECT` — measured, see docs/notes.md. */
|
|
385
|
+
primaryKey: number;
|
|
386
|
+
};
|
|
387
|
+
declare const encodeRef: (id: string) => string;
|
|
388
|
+
declare const decodeRef: (raw: string) => NoteRef;
|
|
389
|
+
/** Build a ref from an index row, which only ever knows the primary key. */
|
|
390
|
+
declare const refFromPrimaryKey: (storeUuid: string, primaryKey: number) => string;
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/server.d.ts
|
|
393
|
+
declare const SERVER_NAME: string;
|
|
394
|
+
declare const SERVER_VERSION: string;
|
|
395
|
+
type CreateServerOptions = {
|
|
396
|
+
config: Config;
|
|
397
|
+
logger?: Logger;
|
|
398
|
+
/** Injected by tests so nothing spawns a process or touches a real Notes. */
|
|
399
|
+
osascript?: OsascriptRunner;
|
|
400
|
+
};
|
|
401
|
+
type CreatedServer = {
|
|
402
|
+
server: McpServer;
|
|
403
|
+
client: AppleNotesClient;
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* Build the server. Side-effect free: it opens no connection, spawns no
|
|
407
|
+
* process and reads no file, so a test can construct it freely and every
|
|
408
|
+
* external dependency arrives through an option.
|
|
409
|
+
*/
|
|
410
|
+
declare const createServer: (opts: CreateServerOptions) => CreatedServer;
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/tools/index.d.ts
|
|
413
|
+
type ToolContext = {
|
|
414
|
+
/**
|
|
415
|
+
* Register the mutating tools too. Off by default — with the flag off they are
|
|
416
|
+
* not merely refused, they are invisible and cannot be called at all.
|
|
417
|
+
*/
|
|
418
|
+
allowWrites: boolean;
|
|
419
|
+
};
|
|
420
|
+
/**
|
|
421
|
+
* Register the Apple Notes tools.
|
|
422
|
+
*
|
|
423
|
+
* The registered set is a pure function of `allowWrites` and nothing else. In
|
|
424
|
+
* particular it does NOT vary with whether Full Disk Access is granted: that is
|
|
425
|
+
* a runtime condition which can change while the process lives, and MCP clients
|
|
426
|
+
* cache the tool list, so a tool that appears and disappears would leave clients
|
|
427
|
+
* calling names the server no longer has. Tools that need the index instead
|
|
428
|
+
* report their source, or explain what is missing.
|
|
429
|
+
*/
|
|
430
|
+
declare const registerTools: (server: McpServer, client: AppleNotesClient, ctx: ToolContext) => void;
|
|
431
|
+
//#endregion
|
|
432
|
+
export { AppleNotesClient, AppleNotesError, BUILD_INFO, type BuildInfo, type Config, type CreateServerOptions, type DecodedNote, type LaneStatus, type LocateResult, NOTES_SURFACE, NOTE_TEXT_PATH, type NoteAccount, type NoteFolder, NoteLockedError, NoteNotFoundError, type NoteRef, type NoteRow, NoteStore, type NoteSummary, NotesBusyError, NotesNotRunningError, REF_VERSION, SERVER_NAME, SERVER_VERSION, type StoreCapabilities, type ToolContext, collectStrings, createServer, decodeRef, defaultStorePath, encodeRef, extractNoteText, introspect, loadConfig, locateStore, openStore, parseFields, readVarint, refFromPrimaryKey, registerTools };
|
|
433
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/build-info.ts","../src/client/errors.ts","../src/client/locate.ts","../src/config.ts","../src/client/store.ts","../src/client/notes.ts","../src/client/protobuf.ts","../src/client/ref.ts","../src/server.ts","../src/tools/index.ts"],"mappings":";;;;;cAkBa,YAAY;;;;cCTZ,eAAe;;cAiBf,0BAA0B;WACnB;EAElB,YAAY;;;;;;;;;cAeD,wBAAwB;WACjB;EAElB,YAAY;;;;KC1BF,eAAe;EACzB;;EAEA;EACA;;cAGW,mBAAgB;cAGhB,cAAW;EACd;EAAgC;MACvC;;;;;;;;;;cCbG,cAAY,EAAA;;;;;;;;;;;;;;;;;;GAiCP,EAAA,KAAA;KAEC,SAAS,EAAE,aAAa;;;;;cAMvB,aAAU,MAAS,OAAO,eAA2B;;;KC1BtD;EACV;EACA;EACA;EACA;EACA;EACA;EACA;;KAGU;EACV;;;;;;EAMA;;EAEA;;EAEA;IACE;IACA;IACA;IACA;IACA;;EAEF;EACA;;cAUW,aAAU,IAAQ,iBAAe;KA4DlC;EACV;EACA;EACA;EACA;EACA;EACA;;cAGW;;WAEF,MAAM;WACN;EAET,YAAY,IAAI,cAAc,MAAM,mBAAmB;EA+CvD;;;;;;;;EAcA,OAAO,SAAS,gBAAgB;EAwChC,aAAa,aAAa;;;;;;;EAa1B,OAAO;IAAe;IAAqB;IAAoB;;EAmB/D;;cAKW,YAAS,cAAgB,MAAQ,cAAY,SAAW,WAAS;;;KCrQlE;EACV;EACA;EACA;EACA;EACA;;KAGU;EACV;EACA;EACA;EACA;EACA;;KAGU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;;KAGU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;KAUU;EACV,QAAQ;EACR,SAAS;;EAET,YAAY;;;;;;;;;;;;;;;cAgBD;;WACF,QAAQ;WACR,QAAQ;EAMjB,YAAY,MAAM;EAalB,UAAU;;EAKV,SAAS;;;;;;;;EAoBH,SAAS,QAAQ;EA8BjB,YAAY,QAAQ;EAKpB,WAAW,QAAQ;;;;;;;;EAYnB,UAAU;IAAQ;IAA6B;MAAkB,QAAQ;;;;;;;;;;EA+GzE,YAAY;IAChB;IACA;IACA;IACA;MACE;IAAU,OAAO;IAAe;IAAkC;;EA4BhE,QACJ,aACA;IAAQ;MACP;IAAU,SAAS;IAAa;IAAqB;;EAwDlD,YAAY,cAAc;IAE5B;IACA;IACA;IACA;;;;;;;;;;EAeE,eACJ,aACA,sBACA,oBACA;IAAQ;MACP;IACD;IACA;;EAsFI,WAAW;IAAQ;IAAgB;IAAe;MAAsB;EAKxE,WAAW,aAAa,cAAc,6BAA6B;EAMnE,SAAS,aAAa,mBAAmB;EAMzC,YAAY,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;KClezB;EAAW;EAAe;;;cAGzB,aAAU,KAAS,YAAU,gBAAgB;KAiB9C;EACV;EACA;EACA;EACA;EACA;;;;;;;cAQW,cAAW,KACjB,YAAU,gBAAA,iBAGd;KAqDS;EAAoB;EAAc;EAAc;;;;;;;;cAQ/C,iBAAc,KAAS,YAAU,sBAAyB;;;;;;;;;;;cA4B1D;KAED;EACV;EACA;;EAEA;EACA;;;;;;;;;cAUW,kBAAe,KACrB,YAAU,+BAEd;;;;;;;;;;;;;;;;;;;cCjKU;KAKD;;EAEV;;EAEA;;cAGW,YAAS;cAET,YAAS,gBAAkB;;cAuB3B,oBAAiB,mBAAqB;;;cChDtC;cACA;KAED;EACV,QAAQ;EACR,SAAS;;EAET,YAAY;;KAGF;EACV,QAAQ;EACR,QAAQ;;;;;;;cAQG,eAAY,MAAU,wBAAsB;;;KCpB7C;;;;;EAKV;;;;;;;;;;;;cAaW,gBAAa,QAChB,WAAS,QACT,kBAAgB,KACnB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { C as NoteLockedError, D as BUILD_INFO, E as NotesNotRunningError, S as NOTES_SURFACE, T as NotesBusyError, _ as encodeRef, a as loadConfig, b as locateStore, c as introspect, d as collectStrings, f as extractNoteText, g as decodeRef, h as REF_VERSION, i as registerTools, l as openStore, m as readVarint, n as SERVER_VERSION, o as AppleNotesClient, p as parseFields, r as createServer, s as NoteStore, t as SERVER_NAME, u as NOTE_TEXT_PATH, v as refFromPrimaryKey, w as NoteNotFoundError, x as AppleNotesError, y as defaultStorePath } from "./server-HLS_dM7r.js";
|
|
2
|
+
export { AppleNotesClient, AppleNotesError, BUILD_INFO, NOTES_SURFACE, NOTE_TEXT_PATH, NoteLockedError, NoteNotFoundError, NoteStore, NotesBusyError, NotesNotRunningError, REF_VERSION, SERVER_NAME, SERVER_VERSION, collectStrings, createServer, decodeRef, defaultStorePath, encodeRef, extractNoteText, introspect, loadConfig, locateStore, openStore, parseFields, readVarint, refFromPrimaryKey, registerTools };
|