apple-voice-memos-mcp 1.0.0

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.
Files changed (59) hide show
  1. package/README.md +63 -0
  2. package/bin/cli.js +352 -0
  3. package/package.json +34 -0
  4. package/src/dist/index.d.ts +3 -0
  5. package/src/dist/index.d.ts.map +1 -0
  6. package/src/dist/index.js +24 -0
  7. package/src/dist/index.js.map +1 -0
  8. package/src/dist/server.d.ts +11 -0
  9. package/src/dist/server.d.ts.map +1 -0
  10. package/src/dist/server.js +96 -0
  11. package/src/dist/server.js.map +1 -0
  12. package/src/dist/services/transcript-extractor.d.ts +6 -0
  13. package/src/dist/services/transcript-extractor.d.ts.map +1 -0
  14. package/src/dist/services/transcript-extractor.js +28 -0
  15. package/src/dist/services/transcript-extractor.js.map +1 -0
  16. package/src/dist/services/transcription-service.d.ts +16 -0
  17. package/src/dist/services/transcription-service.d.ts.map +1 -0
  18. package/src/dist/services/transcription-service.js +81 -0
  19. package/src/dist/services/transcription-service.js.map +1 -0
  20. package/src/dist/services/voice-memo-db.d.ts +19 -0
  21. package/src/dist/services/voice-memo-db.d.ts.map +1 -0
  22. package/src/dist/services/voice-memo-db.js +79 -0
  23. package/src/dist/services/voice-memo-db.js.map +1 -0
  24. package/src/dist/tools/get-audio.d.ts +34 -0
  25. package/src/dist/tools/get-audio.d.ts.map +1 -0
  26. package/src/dist/tools/get-audio.js +68 -0
  27. package/src/dist/tools/get-audio.js.map +1 -0
  28. package/src/dist/tools/get-memo.d.ts +26 -0
  29. package/src/dist/tools/get-memo.d.ts.map +1 -0
  30. package/src/dist/tools/get-memo.js +40 -0
  31. package/src/dist/tools/get-memo.js.map +1 -0
  32. package/src/dist/tools/get-transcript.d.ts +35 -0
  33. package/src/dist/tools/get-transcript.d.ts.map +1 -0
  34. package/src/dist/tools/get-transcript.js +63 -0
  35. package/src/dist/tools/get-transcript.js.map +1 -0
  36. package/src/dist/tools/list-memos.d.ts +39 -0
  37. package/src/dist/tools/list-memos.d.ts.map +1 -0
  38. package/src/dist/tools/list-memos.js +58 -0
  39. package/src/dist/tools/list-memos.js.map +1 -0
  40. package/src/dist/tools/transcribe.d.ts +34 -0
  41. package/src/dist/tools/transcribe.d.ts.map +1 -0
  42. package/src/dist/tools/transcribe.js +64 -0
  43. package/src/dist/tools/transcribe.js.map +1 -0
  44. package/src/dist/types/index.d.ts +56 -0
  45. package/src/dist/types/index.d.ts.map +1 -0
  46. package/src/dist/types/index.js +2 -0
  47. package/src/dist/types/index.js.map +1 -0
  48. package/src/dist/utils/dates.d.ts +17 -0
  49. package/src/dist/utils/dates.d.ts.map +1 -0
  50. package/src/dist/utils/dates.js +57 -0
  51. package/src/dist/utils/dates.js.map +1 -0
  52. package/src/dist/utils/mp4-parser.d.ts +6 -0
  53. package/src/dist/utils/mp4-parser.d.ts.map +1 -0
  54. package/src/dist/utils/mp4-parser.js +109 -0
  55. package/src/dist/utils/mp4-parser.js.map +1 -0
  56. package/src/dist/utils/paths.d.ts +5 -0
  57. package/src/dist/utils/paths.d.ts.map +1 -0
  58. package/src/dist/utils/paths.js +55 -0
  59. package/src/dist/utils/paths.js.map +1 -0
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # apple-voice-memos-mcp
2
+
3
+ MCP server for **Apple Voice Memos** (Memo Vocali) on macOS — with **auto-setup**.
4
+
5
+ ## Quick start (one command)
6
+
7
+ ```bash
8
+ npx apple-voice-memos-mcp --setup
9
+ ```
10
+
11
+ Then add to your MCP config (Claude Desktop, pi, Cursor, etc.):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "apple-voice-memos": {
17
+ "command": "npx",
18
+ "args": ["apple-voice-memos-mcp"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ ## Why the setup?
25
+
26
+ macOS protects Voice Memos data (database + audio) with **Full Disk Access** — a privacy
27
+ permission that, unlike AppleScript automation, **cannot be granted programmatically or
28
+ via automatic prompt**. This package minimizes the manual work to:
29
+
30
+ 1. `npx apple-voice-memos-mcp --setup` — builds a signed helper app
31
+ (`VoiceMemosMCP.app`), registers a LaunchAgent daemon, and opens the Full Disk Access
32
+ panel for you
33
+ 2. Drag **VoiceMemosMCP.app** into the Full Disk Access list (once, ~10 seconds)
34
+ 3. Restart your MCP client (or `/reload`)
35
+
36
+ The architecture (MCP client → stdio → local TCP daemon launched via launchd → signed app)
37
+ is what makes the permission stick reliably — plain `node` processes inherit the
38
+ permissions of their parent, which is why direct setups silently fail.
39
+
40
+ ## MCP Tools
41
+
42
+ - `list_voice_memos` — list memos with metadata (limit/offset/search)
43
+ - `get_voice_memo` — full metadata for one memo
44
+ - `get_audio` — the audio file (path or base64)
45
+ - `get_transcript` — Apple's embedded transcript, if present
46
+ - `transcribe_memo` — on-demand transcription via Apple Speech Recognition
47
+
48
+ ## Status
49
+
50
+ ```bash
51
+ npx apple-voice-memos-mcp --status
52
+ ```
53
+
54
+ ## Requirements
55
+
56
+ - macOS 14+ (Voice Memos app opened at least once)
57
+ - Node.js 18+ (26.x tested — uses better-sqlite3 prebuilds, no compilation)
58
+ - Xcode Command Line Tools (for the 1-second launcher compile during setup)
59
+
60
+ ## Credits
61
+
62
+ Server implementation forked from [jwulff/apple-voice-memo-mcp](https://github.com/jwulff/apple-voice-memo-mcp),
63
+ with a Node 26 compatibility fix (better-sqlite3 13) and the TCC/LaunchAgent architecture.
package/bin/cli.js ADDED
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * apple-voice-memos-mcp — CLI entry point
4
+ *
5
+ * Uso:
6
+ * apple-voice-memos-mcp → avvia il server MCP in stdio (config npx standard)
7
+ * apple-voice-memos-mcp --setup → setup guidato (app firmata + LaunchAgent + pannello FDA)
8
+ * apple-voice-memos-mcp --status → stato del demone e dei permessi
9
+ *
10
+ * Architettura (risolve TCC/Full Disk Access su macOS):
11
+ * gateway MCP → [stdio] → cli.js (questo file) → [TCP 127.0.0.1] → demone (launchd)
12
+ * Il demone è lanciato da launchd come VoiceMemosMCP.app (firmata) → eredita
13
+ * il Full Disk Access e genera un'istanza del server MCP per connessione.
14
+ */
15
+ import { spawn, execSync } from "child_process";
16
+ import { existsSync, mkdirSync, writeFileSync, copyFileSync, readFileSync } from "fs";
17
+ import { join, dirname } from "path";
18
+ import { fileURLToPath } from "url";
19
+ import net from "net";
20
+ import os from "os";
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const HOME = os.homedir();
24
+
25
+ const APP_DIR = join(HOME, "Library", "apple-voice-memos-mcp");
26
+ const APP_PATH = join(APP_DIR, "VoiceMemosMCP.app");
27
+ const APP_EXEC = join(APP_PATH, "Contents", "MacOS", "VoiceMemosMCP");
28
+ const APP_NODE = join(APP_PATH, "Contents", "MacOS", "noded");
29
+ const APP_LIB_DIR = join(APP_PATH, "Contents", "lib");
30
+ const PLIST_PATH = join(HOME, "Library", "LaunchAgents", "it.otomatik.voicememosmcp.plist");
31
+ const TCP_PORT = 8917;
32
+ const TCP_HOST = "127.0.0.1";
33
+ const SERVER_JS = join(__dirname, "..", "src", "dist", "index.js");
34
+
35
+ const PLIST = (nodePath, daemonJs) => `<?xml version="1.0" encoding="UTF-8"?>
36
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
37
+ <plist version="1.0">
38
+ <dict>
39
+ <key>Label</key>
40
+ <string>it.otomatik.voicememosmcp</string>
41
+ <key>ProgramArguments</key>
42
+ <array>
43
+ <string>${nodePath}</string>
44
+ <string>${daemonJs}</string>
45
+ </array>
46
+ <key>RunAtLoad</key>
47
+ <true/>
48
+ <key>KeepAlive</key>
49
+ <true/>
50
+ <key>StandardErrorPath</key>
51
+ <string>/tmp/apple-voice-memos-mcp-daemon.err</string>
52
+ </dict>
53
+ </plist>
54
+ `;
55
+
56
+ const DAEMON_JS = `#!/usr/bin/env node
57
+ // Demone TCP generato da apple-voice-memos-mcp: per ogni connessione
58
+ // lancia un'istanza stdio del server MCP. Lanciato da launchd → eredita FDA.
59
+ const net = require('net');
60
+ const { spawn } = require('child_process');
61
+ const SERVER_JS = ${JSON.stringify(SERVER_JS)};
62
+ const NODE = ${JSON.stringify(process.execPath)};
63
+ const PORT = ${TCP_PORT};
64
+
65
+ const server = net.createServer((socket) => {
66
+ const child = spawn(NODE, [SERVER_JS], { stdio: ['pipe', 'pipe', 'pipe'] });
67
+ socket.pipe(child.stdin);
68
+ child.stdout.pipe(socket);
69
+ child.stderr.on('data', () => {});
70
+ child.on('exit', () => socket.end());
71
+ socket.on('close', () => { try { child.kill(); } catch (e) {} });
72
+ socket.on('error', () => {});
73
+ });
74
+ server.listen(PORT, '127.0.0.1', () => console.error('apple-voice-memos-mcp daemon on :' + PORT));
75
+ `;
76
+
77
+ function log(msg) {
78
+ process.stderr.write(`[apple-voice-memos-mcp] ${msg}\n`);
79
+ }
80
+
81
+ function portOpen(port) {
82
+ return new Promise((resolve) => {
83
+ const sock = net.connect(port, TCP_HOST);
84
+ sock.setTimeout(1500);
85
+ sock.on("connect", () => { sock.destroy(); resolve(true); });
86
+ sock.on("error", () => resolve(false));
87
+ sock.on("timeout", () => { sock.destroy(); resolve(false); });
88
+ });
89
+ }
90
+
91
+ /** Crea (o aggiorna) l'app VoiceMemosMCP.app firmata che avvia il demone */
92
+ function buildApp() {
93
+ // 1. launcher C che esegue node con il demone — compila con cc (presente con Xcode CLT)
94
+ const launcherC = `/tmp/avmm-launcher.c`;
95
+ const launcherBin = `/tmp/avmm-launcher`;
96
+ writeFileSync(
97
+ launcherC,
98
+ `#include <unistd.h>
99
+ #include <stdlib.h>
100
+ #include <libgen.h>
101
+ #include <stdio.h>
102
+ #include <string.h>
103
+ #include <mach-o/dyld.h>
104
+
105
+ int main(int argc, char **argv) {
106
+ char exe_path[4096];
107
+ uint32_t size = sizeof(exe_path);
108
+ if (_NSGetExecutablePath(exe_path, &size) != 0) return 1;
109
+ char *dir = dirname(exe_path); // Contents/MacOS
110
+
111
+ char node_path[4096];
112
+ snprintf(node_path, sizeof(node_path), "%s/noded", dir);
113
+
114
+ char server_path[4096];
115
+ snprintf(server_path, sizeof(server_path), "%s/dist/index.js", dir);
116
+
117
+ char libdir[4096];
118
+ snprintf(libdir, sizeof(libdir), "%s/../lib", dir);
119
+ setenv("DYLD_LIBRARY_PATH", libdir, 1);
120
+
121
+ execl(node_path, "noded", server_path, (char *)NULL);
122
+ perror("execl failed");
123
+ return 1;
124
+ }
125
+ `
126
+ );
127
+ execSync(`cc -o ${launcherBin} ${launcherC}`);
128
+
129
+ // 2. struttura bundle
130
+ mkdirSync(join(APP_PATH, "Contents", "MacOS"), { recursive: true });
131
+ mkdirSync(join(APP_PATH, "Contents", "lib"), { recursive: true });
132
+
133
+ writeFileSync(
134
+ join(APP_PATH, "Contents", "Info.plist"),
135
+ `<?xml version="1.0" encoding="UTF-8"?>
136
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
137
+ <plist version="1.0">
138
+ <dict>
139
+ <key>CFBundleIdentifier</key>
140
+ <string>it.otomatik.voicememosmcp</string>
141
+ <key>CFBundleName</key>
142
+ <string>VoiceMemosMCP</string>
143
+ <key>CFBundleExecutable</key>
144
+ <string>VoiceMemosMCP</string>
145
+ <key>CFBundlePackageType</key>
146
+ <string>APPL</string>
147
+ <key>CFBundleVersion</key>
148
+ <string>1.0.0</string>
149
+ <key>CFBundleShortVersionString</key>
150
+ <string>1.0.0</string>
151
+ <key>LSUIElement</key>
152
+ <true/>
153
+ </dict>
154
+ </plist>
155
+ `
156
+ );
157
+
158
+ copyFileSync(launcherBin, APP_EXEC);
159
+ chmod(APP_EXEC, 0o755);
160
+
161
+ // 3. copia node + libnode DENTRO l'app (binario dedicato per TCC)
162
+ const nodeBin = process.execPath;
163
+ copyFileSync(nodeBin, APP_NODE);
164
+ chmod(APP_NODE, 0o755);
165
+
166
+ // libnode: cerca TUTTE le libnode*.dylib e le copia con il nome originale
167
+ try {
168
+ const realNode = execSync(`readlink -f ${nodeBin}`).toString().trim();
169
+ const nodeDir = dirname(realNode); // .../bin
170
+ mkdirSync(APP_LIB_DIR, { recursive: true });
171
+ try {
172
+ const libDir = join(nodeDir, "..", "lib");
173
+ const libs = execSync(`ls ${libDir}/libnode*.dylib 2>/dev/null`).toString().trim().split("\n").filter(Boolean);
174
+ for (const lib of libs) {
175
+ const dest = join(APP_LIB_DIR, lib.split("/").pop());
176
+ copyFileSync(lib, dest);
177
+ chmod(dest, 0o755);
178
+ }
179
+ } catch (e) { /* node statico o bundled: continua */ }
180
+
181
+ // rpath del nodo copiato → punta a ../lib dentro il bundle
182
+ try {
183
+ execSync(`install_name_tool -add_rpath "@executable_path/../lib" ${APP_NODE} 2>/dev/null || true`);
184
+ } catch (e) { /* rpath già presente */ }
185
+ } catch (e) { /* readlink fallito: node forse statico */ }
186
+
187
+ // 4. copia l'INTERA dist del server dentro Contents/MacOS/dist (entry index.js)
188
+ // I moduli (better-sqlite3, sdk, zod) si risolvono via NODE_PATH verso i node_modules del pacchetto
189
+ const pkgRoot = join(__dirname, "..");
190
+ const distSrc = join(pkgRoot, "src", "dist");
191
+ const distDst = join(APP_PATH, "Contents", "MacOS", "dist");
192
+ mkdirSync(distDst, { recursive: true });
193
+ execSync(`cp -R "${distSrc}/." "${distDst}/"`);
194
+ // ESM risolve i bare imports SOLO dal node_modules relativo: creo un symlink
195
+ // Contents/MacOS/dist/node_modules → node_modules del pacchetto installato
196
+ try { execSync(`rm -f "${join(distDst, 'node_modules')}"`); } catch (e) {}
197
+ execSync(`ln -s "${join(pkgRoot, 'node_modules')}" "${join(distDst, 'node_modules')}"`);
198
+
199
+ // 5. firma
200
+ execSync(`codesign --force --deep --sign - "${APP_PATH}" 2>/dev/null`);
201
+ }
202
+
203
+ function chmod(p, mode) {
204
+ try { execSync(`chmod ${mode.toString(8)} "${p}"`); } catch (e) {}
205
+ }
206
+
207
+ /** Registra il LaunchAgent e avvia il demone */
208
+ function installDaemon() {
209
+ writeFileSync(join(APP_DIR, "daemon.js"), DAEMON_JS);
210
+ writeFileSync(PLIST_PATH, PLIST(APP_NODE, join(APP_DIR, "daemon.js")));
211
+ try { execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`); } catch (e) {}
212
+ execSync(`launchctl load "${PLIST_PATH}"`);
213
+ }
214
+
215
+ /** Setup guidato */
216
+ async function setup() {
217
+ log("Setup apple-voice-memos-mcp…");
218
+ log("1/4 · Compilo il launcher e creo l'app VoiceMemosMCP.app…");
219
+ buildApp();
220
+ log(" ✓ " + APP_PATH);
221
+
222
+ log("2/4 · Registro il LaunchAgent…");
223
+ installDaemon();
224
+ log(" ✓ demone TCP su " + TCP_HOST + ":" + TCP_PORT);
225
+
226
+ // attesa che il demone risponda
227
+ const up = await portOpen(TCP_PORT);
228
+ log(up ? " ✓ demone attivo" : " ⚠ demone non risponde (riproverà il gateway)");
229
+
230
+ log("3/4 · Permessi macOS (Full Disk Access)…");
231
+ log("");
232
+ log(" ╔══════════════════════════════════════════════════════════╗");
233
+ log(" ║ AZIONE MANUALE RICHIESTA (una volta sola, 10 secondi) ║");
234
+ log(" ║ ║");
235
+ log(" ║ Nel pannello che sto per aprire: ║");
236
+ log(" ║ 1. clicca \"+\" ║");
237
+ log(" ║ 2. premi Cmd+Shift+G e incolla: ║");
238
+ log(` ║ ${APP_PATH.slice(0, 42)} ║`);
239
+ log(` ║ ${APP_PATH.slice(42)} ║`);
240
+ log(" ║ 3. seleziona VoiceMemosMCP.app e attivala ║");
241
+ log(" ╚══════════════════════════════════════════════════════════╝");
242
+ log("");
243
+ try { execSync(`open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"`); } catch (e) {}
244
+ log("4/4 · Al termine riavvia il client MCP (o esegui /reload). Fatto! ✅");
245
+ }
246
+
247
+ /** Status */
248
+ async function status() {
249
+ const up = await portOpen(TCP_PORT);
250
+ console.log("Demone TCP (" + TCP_HOST + ":" + TCP_PORT + "): " + (up ? "ATTIVO ✅" : "NON ATTIVO ❌"));
251
+ console.log("App: " + (existsSync(APP_PATH) ? "presente ✅" : "assente ❌") + " → " + APP_PATH);
252
+ console.log("LaunchAgent: " + (existsSync(PLIST_PATH) ? "registrato ✅" : "assente ❌"));
253
+ // test lettura DB (se il demone è attivo, chiede la lista)
254
+ if (up) {
255
+ try {
256
+ const res = await callMcp("list_voice_memos", { limit: 1 });
257
+ console.log("Accesso al DB Memo Vocali: OK ✅ (" + res.total + " memo)");
258
+ } catch (e) {
259
+ console.log("Accesso al DB Memo Vocali: NEGATO ❌ → manca Full Disk Access su VoiceMemosMCP.app");
260
+ console.log(" Esegui: apple-voice-memos-mcp --setup");
261
+ }
262
+ }
263
+ }
264
+
265
+ function callMcp(tool, args) {
266
+ return new Promise((resolve, reject) => {
267
+ const sock = net.connect(TCP_PORT, TCP_HOST);
268
+ let buf = "";
269
+ sock.on("connect", () => {
270
+ sock.write(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "status", version: "1" } } }) + "\n");
271
+ setTimeout(() => {
272
+ sock.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
273
+ sock.write(JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: tool, arguments: args } }) + "\n");
274
+ }, 300);
275
+ });
276
+ sock.on("data", (d) => {
277
+ buf += d.toString();
278
+ const lines = buf.split("\n");
279
+ buf = lines.pop();
280
+ for (const line of lines) {
281
+ try {
282
+ const msg = JSON.parse(line);
283
+ if (msg.id === 2) {
284
+ sock.destroy();
285
+ if (msg.result?.isError) return reject(new Error(msg.result.content?.[0]?.text));
286
+ return resolve(JSON.parse(msg.result.content[0].text));
287
+ }
288
+ } catch (e) {}
289
+ }
290
+ });
291
+ sock.on("error", reject);
292
+ setTimeout(() => { sock.destroy(); reject(new Error("timeout")); }, 8000);
293
+ });
294
+ }
295
+
296
+ /** Modalità server stdio per il gateway MCP: bridge verso il demone TCP con auto-bootstrap */
297
+ async function serveStdio() {
298
+ // se il demone non risponde, prova a installarlo/riavviarlo al volo
299
+ let up = await portOpen(TCP_PORT);
300
+ if (!up && existsSync(APP_PATH) && existsSync(PLIST_PATH)) {
301
+ try {
302
+ execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null; launchctl load "${PLIST_PATH}"`);
303
+ await new Promise((r) => setTimeout(r, 2000));
304
+ up = await portOpen(TCP_PORT);
305
+ } catch (e) {}
306
+ }
307
+ if (!up) {
308
+ process.stderr.write(
309
+ "\n[apple-voice-memos-mcp] Demone non attivo. Esegui il setup una volta sola:\n" +
310
+ " npx apple-voice-memos-mcp --setup\n\n"
311
+ );
312
+ // resta comunque in stdio e prova a servire localmente senza demone (fallirà sui dati ma il client MCP non crasha)
313
+ }
314
+
315
+ if (up) {
316
+ // bridge stdio <-> TCP
317
+ const sock = net.connect(TCP_PORT, TCP_HOST);
318
+ sock.on("error", () => process.exit(1));
319
+ process.stdin.pipe(sock);
320
+ sock.pipe(process.stdout);
321
+ process.stdin.on("end", () => sock.end());
322
+ sock.on("close", () => process.exit(0));
323
+ } else {
324
+ // echo server minimo per non far crashare il gateway
325
+ let b = "";
326
+ process.stdin.on("data", (d) => {
327
+ b += d.toString();
328
+ let i;
329
+ while ((i = b.indexOf("\n")) !== -1) {
330
+ const line = b.slice(0, i); b = b.slice(i + 1);
331
+ try {
332
+ const msg = JSON.parse(line);
333
+ if (msg.id !== undefined) {
334
+ process.stdout.write(JSON.stringify({
335
+ jsonrpc: "2.0", id: msg.id,
336
+ error: { code: -32000, message: "daemon not running — run: npx apple-voice-memos-mcp --setup" }
337
+ }) + "\n");
338
+ }
339
+ } catch (e) {}
340
+ }
341
+ });
342
+ }
343
+ }
344
+
345
+ const arg = process.argv[2];
346
+ if (arg === "--setup" || arg === "-s") {
347
+ setup().catch((e) => { log("ERRORE: " + e.message); process.exit(1); });
348
+ } else if (arg === "--status") {
349
+ status().catch((e) => { log("ERRORE: " + e.message); process.exit(1); });
350
+ } else {
351
+ serveStdio();
352
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "apple-voice-memos-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Apple Voice Memos on macOS \u2014 zero-config npx, auto-setup with one-click Full Disk Access",
5
+ "bin": {
6
+ "apple-voice-memos-mcp": "bin/cli.js"
7
+ },
8
+ "main": "bin/cli.js",
9
+ "files": [
10
+ "bin/",
11
+ "src/"
12
+ ],
13
+ "keywords": [
14
+ "mcp",
15
+ "apple",
16
+ "voice-memos",
17
+ "macos",
18
+ "model-context-protocol",
19
+ "transcription"
20
+ ],
21
+ "license": "MIT",
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "dependencies": {
26
+ "@modelcontextprotocol/sdk": "^1.0.0",
27
+ "better-sqlite3": "^13.0.0",
28
+ "zod": "^3.25.0"
29
+ },
30
+ "overrides": {
31
+ "better-sqlite3": "^13.0.0"
32
+ },
33
+ "type": "module"
34
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { VoiceMemoMCPServer } from "./server.js";
3
+ async function main() {
4
+ const server = new VoiceMemoMCPServer();
5
+ // Handle graceful shutdown
6
+ process.on("SIGINT", () => {
7
+ server.close();
8
+ process.exit(0);
9
+ });
10
+ process.on("SIGTERM", () => {
11
+ server.close();
12
+ process.exit(0);
13
+ });
14
+ try {
15
+ await server.run();
16
+ }
17
+ catch (error) {
18
+ console.error("Fatal error:", error);
19
+ server.close();
20
+ process.exit(1);
21
+ }
22
+ }
23
+ main();
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,IAAI,kBAAkB,EAAE,CAAC;IAExC,2BAA2B;IAC3B,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC;IACrB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC"}
@@ -0,0 +1,11 @@
1
+ export declare class VoiceMemoMCPServer {
2
+ private server;
3
+ private db;
4
+ private transcriptExtractor;
5
+ private transcriptionService;
6
+ constructor();
7
+ private setupHandlers;
8
+ run(): Promise<void>;
9
+ close(): void;
10
+ }
11
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAqCA,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,mBAAmB,CAAsB;IACjD,OAAO,CAAC,oBAAoB,CAAuB;;IAwBnD,OAAO,CAAC,aAAa;IAyEf,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAK1B,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,96 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { VoiceMemoDatabase } from "./services/voice-memo-db.js";
5
+ import { TranscriptExtractor } from "./services/transcript-extractor.js";
6
+ import { TranscriptionService } from "./services/transcription-service.js";
7
+ import { listMemos, listMemosSchema, listMemosToolDefinition, } from "./tools/list-memos.js";
8
+ import { getMemo, getMemoSchema, getMemoToolDefinition, } from "./tools/get-memo.js";
9
+ import { getAudio, getAudioSchema, getAudioToolDefinition, } from "./tools/get-audio.js";
10
+ import { getTranscript, getTranscriptSchema, getTranscriptToolDefinition, } from "./tools/get-transcript.js";
11
+ import { transcribeMemo, transcribeSchema, transcribeToolDefinition, } from "./tools/transcribe.js";
12
+ export class VoiceMemoMCPServer {
13
+ server;
14
+ db;
15
+ transcriptExtractor;
16
+ transcriptionService;
17
+ constructor() {
18
+ this.server = new Server({
19
+ name: "apple-voice-memo-mcp",
20
+ version: "0.1.0",
21
+ }, {
22
+ capabilities: {
23
+ tools: {},
24
+ },
25
+ });
26
+ this.db = new VoiceMemoDatabase();
27
+ this.transcriptExtractor = new TranscriptExtractor();
28
+ this.transcriptionService = new TranscriptionService(process.env.VOICE_MEMO_TRANSCRIBER_PATH);
29
+ this.setupHandlers();
30
+ }
31
+ setupHandlers() {
32
+ // List available tools
33
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
34
+ tools: [
35
+ listMemosToolDefinition,
36
+ getMemoToolDefinition,
37
+ getAudioToolDefinition,
38
+ getTranscriptToolDefinition,
39
+ transcribeToolDefinition,
40
+ ],
41
+ }));
42
+ // Handle tool calls
43
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
44
+ const { name, arguments: args } = request.params;
45
+ try {
46
+ switch (name) {
47
+ case "list_voice_memos": {
48
+ const input = listMemosSchema.parse(args);
49
+ const result = await listMemos(input, this.db);
50
+ return { content: [{ type: "text", text: result }] };
51
+ }
52
+ case "get_voice_memo": {
53
+ const input = getMemoSchema.parse(args);
54
+ const result = await getMemo(input, this.db);
55
+ return { content: [{ type: "text", text: result }] };
56
+ }
57
+ case "get_audio": {
58
+ const input = getAudioSchema.parse(args);
59
+ const result = await getAudio(input, this.db);
60
+ return { content: [{ type: "text", text: result }] };
61
+ }
62
+ case "get_transcript": {
63
+ const input = getTranscriptSchema.parse(args);
64
+ const result = await getTranscript(input, this.db, this.transcriptExtractor);
65
+ return { content: [{ type: "text", text: result }] };
66
+ }
67
+ case "transcribe_memo": {
68
+ const input = transcribeSchema.parse(args);
69
+ const result = await transcribeMemo(input, this.db, this.transcriptionService);
70
+ return { content: [{ type: "text", text: result }] };
71
+ }
72
+ default:
73
+ return {
74
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
75
+ isError: true,
76
+ };
77
+ }
78
+ }
79
+ catch (error) {
80
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
81
+ return {
82
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
83
+ isError: true,
84
+ };
85
+ }
86
+ });
87
+ }
88
+ async run() {
89
+ const transport = new StdioServerTransport();
90
+ await this.server.connect(transport);
91
+ }
92
+ close() {
93
+ this.db.close();
94
+ }
95
+ }
96
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { Transcript } from "../types/index.js";
2
+ export declare class TranscriptExtractor {
3
+ extractTranscript(memoPath: string, format?: "text" | "json" | "timestamped"): Transcript | null;
4
+ extractTranscriptById(memoId: number, memoPath: string, format?: "text" | "json" | "timestamped"): Transcript | null;
5
+ }
6
+ //# sourceMappingURL=transcript-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcript-extractor.d.ts","sourceRoot":"","sources":["../../src/services/transcript-extractor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,qBAAa,mBAAmB;IAC9B,iBAAiB,CACf,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,MAAM,GAAG,MAAM,GAAG,aAAsB,GAC/C,UAAU,GAAG,IAAI;IA4BpB,qBAAqB,CACnB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,MAAM,GAAG,MAAM,GAAG,aAAsB,GAC/C,UAAU,GAAG,IAAI;CAGrB"}
@@ -0,0 +1,28 @@
1
+ import { getAudioFilePath } from "../utils/paths.js";
2
+ import { extractTsrpAtom, parseTsrpToText, parseTsrpToSegments, } from "../utils/mp4-parser.js";
3
+ export class TranscriptExtractor {
4
+ extractTranscript(memoPath, format = "text") {
5
+ const fullPath = getAudioFilePath(memoPath);
6
+ if (!fullPath) {
7
+ return null;
8
+ }
9
+ const tsrpData = extractTsrpAtom(fullPath);
10
+ if (!tsrpData) {
11
+ return null;
12
+ }
13
+ const text = parseTsrpToText(tsrpData);
14
+ const locale = tsrpData.locale?.identifier || "unknown";
15
+ if (format === "text") {
16
+ return { text, locale };
17
+ }
18
+ if (format === "timestamped" || format === "json") {
19
+ const segments = parseTsrpToSegments(tsrpData);
20
+ return { text, segments, locale };
21
+ }
22
+ return { text, locale };
23
+ }
24
+ extractTranscriptById(memoId, memoPath, format = "text") {
25
+ return this.extractTranscript(memoPath, format);
26
+ }
27
+ }
28
+ //# sourceMappingURL=transcript-extractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcript-extractor.js","sourceRoot":"","sources":["../../src/services/transcript-extractor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EACL,eAAe,EACf,eAAe,EACf,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAGhC,MAAM,OAAO,mBAAmB;IAC9B,iBAAiB,CACf,QAAgB,EAChB,SAA0C,MAAM;QAEhD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAE5C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QAE3C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,UAAU,IAAI,SAAS,CAAC;QAExD,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACtB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAC1B,CAAC;QAED,IAAI,MAAM,KAAK,aAAa,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YAClD,MAAM,QAAQ,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;YAC/C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QACpC,CAAC;QAED,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1B,CAAC;IAED,qBAAqB,CACnB,MAAc,EACd,QAAgB,EAChB,SAA0C,MAAM;QAEhD,OAAO,IAAI,CAAC,iBAAiB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAClD,CAAC;CACF"}
@@ -0,0 +1,16 @@
1
+ import type { TranscriptionResult } from "../types/index.js";
2
+ /**
3
+ * Service for transcribing audio files using Apple's SFSpeechRecognizer.
4
+ *
5
+ * This requires a Swift helper CLI tool to be built and available.
6
+ * The helper uses SFSpeechRecognizer which requires:
7
+ * 1. Speech Recognition permission granted
8
+ * 2. Language packs downloaded via System Settings > Accessibility > Voice Control
9
+ */
10
+ export declare class TranscriptionService {
11
+ private swiftHelperPath;
12
+ constructor(swiftHelperPath?: string);
13
+ transcribe(memoPath: string, language?: string): Promise<TranscriptionResult>;
14
+ private runSwiftTranscriber;
15
+ }
16
+ //# sourceMappingURL=transcription-service.d.ts.map