imessage-mcp-server 1.0.0 → 2.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.
package/index.js DELETED
@@ -1,480 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import {
5
- CallToolRequestSchema,
6
- ListToolsRequestSchema,
7
- } from "@modelcontextprotocol/sdk/types.js";
8
- import Database from "better-sqlite3";
9
- import { execSync } from "child_process";
10
- import { existsSync, readFileSync } from "fs";
11
- import { homedir } from "os";
12
- import { resolve } from "path";
13
- import { fileURLToPath } from "url";
14
-
15
- // ─── Startup Checks ─────────────────────────────────────────────────────────
16
-
17
- const __dirname = resolve(fileURLToPath(import.meta.url), "..");
18
-
19
- // --version / --help
20
- if (process.argv.includes("--version") || process.argv.includes("-v")) {
21
- const pkg = JSON.parse(
22
- readFileSync(resolve(__dirname, "package.json"), "utf-8")
23
- );
24
- console.log(pkg.version);
25
- process.exit(0);
26
- }
27
-
28
- if (process.argv.includes("--help") || process.argv.includes("-h")) {
29
- console.log(`
30
- imessage-mcp-server — MCP server for iMessage on macOS
31
-
32
- Usage:
33
- npx imessage-mcp-server Start the MCP server (stdio)
34
- npx imessage-mcp-server --help Show this help
35
- npx imessage-mcp-server --version Show version
36
-
37
- Environment variables:
38
- IMESSAGE_DB_PATH Override the path to chat.db (default: ~/Library/Messages/chat.db)
39
- `);
40
- process.exit(0);
41
- }
42
-
43
- // macOS check
44
- if (process.platform !== "darwin") {
45
- console.error(
46
- "❌ iMessage MCP only supports macOS (detected: " + process.platform + ")"
47
- );
48
- process.exit(1);
49
- }
50
-
51
- // osascript check
52
- try {
53
- execSync("which osascript", { encoding: "utf-8", stdio: "pipe" });
54
- } catch {
55
- console.error("❌ osascript not found. This tool requires macOS.");
56
- process.exit(1);
57
- }
58
-
59
- // ─── Constants ──────────────────────────────────────────────────────────────
60
-
61
- const DB_PATH = resolve(
62
- process.env.IMESSAGE_DB_PATH || resolve(homedir(), "Library/Messages/chat.db")
63
- );
64
-
65
- // Database file check
66
- if (!existsSync(DB_PATH)) {
67
- console.error("❌ iMessage database not found at: " + DB_PATH);
68
- console.error(" Make sure you are signed into iMessage on this Mac.");
69
- console.error(
70
- " If the database is elsewhere, set IMESSAGE_DB_PATH to its location."
71
- );
72
- process.exit(1);
73
- }
74
- // Apple Cocoa epoch: 2001-01-01 00:00:00 UTC
75
- const COCOA_EPOCH_OFFSET_S = 978_307_200;
76
-
77
- // ─── Helpers ────────────────────────────────────────────────────────────────
78
-
79
- function openDb() {
80
- const db = new Database(DB_PATH, { readonly: true, fileMustExist: true });
81
- db.pragma("journal_mode = WAL");
82
- return db;
83
- }
84
-
85
- function cocoaDateToISO(cocoaNs) {
86
- if (!cocoaNs || cocoaNs <= 0) return null;
87
- const unixMs = (cocoaNs / 1_000_000_000 + COCOA_EPOCH_OFFSET_S) * 1000;
88
- return new Date(unixMs).toISOString();
89
- }
90
-
91
- /**
92
- * Send an iMessage via AppleScript.
93
- * Uses the 'Messages' app which must be signed into iMessage.
94
- */
95
- function sendMessage(recipient, text) {
96
- // Escape double quotes in text for AppleScript
97
- const escaped = text.replace(/"/g, '\\"');
98
- const script = `tell application "Messages" to send "${escaped}" to buddy "${recipient}"`;
99
- execSync(`osascript -e '${script}'`, {
100
- encoding: "utf-8",
101
- timeout: 15_000,
102
- });
103
- return { success: true, recipient, text };
104
- }
105
-
106
- // ─── Tool Implementations ───────────────────────────────────────────────────
107
-
108
- const TOOLS = {
109
- send_imessage: {
110
- description: "Send an iMessage to a recipient's email or phone number",
111
- inputSchema: {
112
- type: "object",
113
- properties: {
114
- recipient: {
115
- type: "string",
116
- description: "Recipient iMessage account (email or phone number)",
117
- },
118
- text: {
119
- type: "string",
120
- description: "Message text to send",
121
- },
122
- },
123
- required: ["recipient", "text"],
124
- },
125
- handler: (args) => {
126
- const result = sendMessage(args.recipient, args.text);
127
- return {
128
- content: [
129
- {
130
- type: "text",
131
- text: JSON.stringify(result),
132
- },
133
- ],
134
- };
135
- },
136
- },
137
-
138
- list_conversations: {
139
- description:
140
- "List recent iMessage conversations with their latest message preview and unread count",
141
- inputSchema: {
142
- type: "object",
143
- properties: {
144
- limit: {
145
- type: "number",
146
- description: "Max conversations to return (default 20)",
147
- default: 20,
148
- },
149
- },
150
- },
151
- handler: (args) => {
152
- const limit = Math.min(args.limit ?? 20, 100);
153
- const db = openDb();
154
- const rows = db
155
- .prepare(
156
- `
157
- SELECT
158
- c.ROWID AS chat_id,
159
- c.display_name,
160
- c.chat_identifier,
161
- c.service_name,
162
- h.id AS handle_id_str,
163
- m.ROWID AS last_msg_id,
164
- m.text AS last_msg_text,
165
- m.date AS last_msg_date,
166
- m.is_from_me AS last_msg_from_me,
167
- (SELECT COUNT(*) FROM message
168
- WHERE handle_id = h.ROWID
169
- AND is_read = 0 AND is_from_me = 0
170
- AND is_finished = 1
171
- ) AS unread_count
172
- FROM chat c
173
- JOIN chat_handle_join chj ON chj.chat_id = c.ROWID
174
- JOIN handle h ON h.ROWID = chj.handle_id
175
- LEFT JOIN (
176
- SELECT cmj.chat_id, MAX(m.ROWID) AS max_msg_id
177
- FROM chat_message_join cmj
178
- JOIN message m ON m.ROWID = cmj.message_id
179
- GROUP BY cmj.chat_id
180
- ) latest ON latest.chat_id = c.ROWID
181
- LEFT JOIN message m ON m.ROWID = latest.max_msg_id
182
- ORDER BY COALESCE(m.date, 0) DESC
183
- LIMIT ?
184
- `
185
- )
186
- .all(limit);
187
- db.close();
188
-
189
- const conversations = rows.map((r) => ({
190
- chat_id: r.chat_id,
191
- display_name: r.display_name || r.chat_identifier || r.handle_id_str,
192
- handle: r.handle_id_str,
193
- service: r.service_name,
194
- unread_count: r.unread_count || 0,
195
- last_message: r.last_msg_text
196
- ? {
197
- text: r.last_msg_text.substring(0, 200),
198
- date: cocoaDateToISO(r.last_msg_date),
199
- is_from_me: !!r.last_msg_from_me,
200
- }
201
- : null,
202
- }));
203
-
204
- return {
205
- content: [
206
- {
207
- type: "text",
208
- text: JSON.stringify(conversations, null, 2),
209
- },
210
- ],
211
- };
212
- },
213
- },
214
-
215
- read_conversation: {
216
- description:
217
- "Read messages from a specific conversation by chat_id or handle (email/phone). Returns paginated messages.",
218
- inputSchema: {
219
- type: "object",
220
- properties: {
221
- handle: {
222
- type: "string",
223
- description:
224
- "Filter by handle (email or phone number of the conversation partner)",
225
- },
226
- chat_id: {
227
- type: "number",
228
- description: "Filter by chat ROWID",
229
- },
230
- limit: {
231
- type: "number",
232
- description: "Max messages to return (default 30)",
233
- default: 30,
234
- },
235
- before_id: {
236
- type: "number",
237
- description:
238
- "Return messages before this ROWID (for pagination/older messages)",
239
- },
240
- include_read: {
241
- type: "boolean",
242
- description: "Include already-read messages (default true)",
243
- default: true,
244
- },
245
- unread_only: {
246
- type: "boolean",
247
- description: "Only return unread messages (default false)",
248
- default: false,
249
- },
250
- },
251
- },
252
- handler: (args) => {
253
- const limit = Math.min(args.limit ?? 30, 200);
254
- const includeRead = args.include_read !== false;
255
- const unreadOnly = args.unread_only === true;
256
-
257
- const db = openDb();
258
-
259
- let where = "WHERE 1=1";
260
- const params = [];
261
-
262
- if (args.handle) {
263
- where += " AND h.id = ?";
264
- params.push(args.handle);
265
- }
266
- if (args.chat_id) {
267
- where += " AND cmj.chat_id = ?";
268
- params.push(args.chat_id);
269
- }
270
- if (args.before_id) {
271
- where += " AND m.ROWID < ?";
272
- params.push(args.before_id);
273
- }
274
- if (unreadOnly) {
275
- where += " AND m.is_read = 0 AND m.is_from_me = 0 AND m.is_finished = 1";
276
- } else if (!includeRead) {
277
- where += " AND m.is_read = 0 AND m.is_from_me = 0";
278
- }
279
-
280
- const rows = db
281
- .prepare(
282
- `
283
- SELECT
284
- m.ROWID,
285
- m.text,
286
- m.is_from_me,
287
- m.is_read,
288
- m.is_delivered,
289
- m.date,
290
- m.service,
291
- m.date_read,
292
- m.date_delivered,
293
- h.id AS handle_id_str,
294
- c.ROWID AS chat_id,
295
- COALESCE(c.display_name, c.chat_identifier) AS chat_name
296
- FROM message m
297
- JOIN chat_message_join cmj ON cmj.message_id = m.ROWID
298
- JOIN chat c ON c.ROWID = cmj.chat_id
299
- LEFT JOIN handle h ON h.ROWID = m.handle_id
300
- ${where}
301
- ORDER BY m.date DESC
302
- LIMIT ?
303
- `
304
- )
305
- .all(...params, limit);
306
- db.close();
307
-
308
- const messages = rows
309
- .map((r) => ({
310
- id: r.ROWID,
311
- chat_id: r.chat_id,
312
- chat_name: r.chat_name,
313
- text: r.text,
314
- from_me: !!r.is_from_me,
315
- from: r.is_from_me ? "me" : r.handle_id_str,
316
- is_read: !!r.is_read,
317
- is_delivered: !!r.is_delivered,
318
- service: r.service,
319
- date: cocoaDateToISO(r.date),
320
- date_read: cocoaDateToISO(r.date_read),
321
- date_delivered: cocoaDateToISO(r.date_delivered),
322
- }))
323
- .reverse();
324
-
325
- return {
326
- content: [
327
- {
328
- type: "text",
329
- text: JSON.stringify(
330
- {
331
- chat_id: rows[0]?.chat_id ?? null,
332
- chat_name: rows[0]?.chat_name ?? null,
333
- total: messages.length,
334
- messages,
335
- },
336
- null,
337
- 2
338
- ),
339
- },
340
- ],
341
- };
342
- },
343
- },
344
-
345
- get_new_messages: {
346
- description:
347
- "Get recently received messages since a given timestamp. Use this to poll for new incoming iMessages.",
348
- inputSchema: {
349
- type: "object",
350
- properties: {
351
- since: {
352
- type: "string",
353
- description:
354
- "ISO timestamp to fetch messages from (e.g., '2026-06-28T10:00:00.000Z'). If omitted, returns last 10 messages.",
355
- },
356
- mark_read: {
357
- type: "boolean",
358
- description:
359
- "Whether to mark unread messages as read in chat.db (default false). NOTE: This modifies the database.",
360
- default: false,
361
- },
362
- max_results: {
363
- type: "number",
364
- description: "Max messages to return (default 10)",
365
- default: 10,
366
- },
367
- },
368
- },
369
- handler: (args) => {
370
- const maxResults = Math.min(args.max_results ?? 10, 100);
371
- const markRead = args.mark_read === true;
372
- const db = openDb();
373
-
374
- let where = "WHERE m.is_from_me = 0 AND m.is_finished = 1";
375
-
376
- if (args.since) {
377
- const sinceDate = new Date(args.since);
378
- if (!isNaN(sinceDate.getTime())) {
379
- const cocoaNs =
380
- (sinceDate.getTime() / 1000 - COCOA_EPOCH_OFFSET_S) * 1_000_000_000;
381
- where += ` AND m.date > ${Math.floor(cocoaNs)}`;
382
- }
383
- }
384
-
385
- const rows = db
386
- .prepare(
387
- `
388
- SELECT
389
- m.ROWID,
390
- m.text,
391
- m.is_from_me,
392
- m.is_read,
393
- m.is_delivered,
394
- m.date,
395
- m.service,
396
- h.id AS handle_id_str,
397
- c.ROWID AS chat_id,
398
- COALESCE(c.display_name, c.chat_identifier) AS chat_name
399
- FROM message m
400
- JOIN chat_message_join cmj ON cmj.message_id = m.ROWID
401
- JOIN chat c ON c.ROWID = cmj.chat_id
402
- LEFT JOIN handle h ON h.ROWID = m.handle_id
403
- ${where}
404
- ORDER BY m.date DESC
405
- LIMIT ?
406
- `
407
- )
408
- .all(maxResults);
409
- db.close();
410
-
411
- const messages = rows.map((r) => ({
412
- id: r.ROWID,
413
- chat_id: r.chat_id,
414
- chat_name: r.chat_name,
415
- text: r.text,
416
- from: r.handle_id_str,
417
- is_read: !!r.is_read,
418
- is_delivered: !!r.is_delivered,
419
- service: r.service,
420
- date: cocoaDateToISO(r.date),
421
- }));
422
-
423
- return {
424
- content: [
425
- {
426
- type: "text",
427
- text: JSON.stringify(
428
- {
429
- total: messages.length,
430
- has_unread: messages.some((m) => !m.is_read),
431
- messages,
432
- },
433
- null,
434
- 2
435
- ),
436
- },
437
- ],
438
- };
439
- },
440
- },
441
- };
442
-
443
- // ─── MCP Server ─────────────────────────────────────────────────────────────
444
-
445
- const server = new Server(
446
- {
447
- name: "imessage-mcp",
448
- version: "1.0.0",
449
- },
450
- {
451
- capabilities: {
452
- tools: {},
453
- },
454
- }
455
- );
456
-
457
- server.setRequestHandler(ListToolsRequestSchema, async () => {
458
- return {
459
- tools: Object.entries(TOOLS).map(([name, tool]) => ({
460
- name,
461
- description: tool.description,
462
- inputSchema: tool.inputSchema,
463
- })),
464
- };
465
- });
466
-
467
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
468
- const { name, arguments: args } = request.params;
469
- const tool = TOOLS[name];
470
- if (!tool) {
471
- throw new Error(`Unknown tool: ${name}`);
472
- }
473
- return tool.handler(args ?? {});
474
- });
475
-
476
- // ─── Startup ────────────────────────────────────────────────────────────────
477
-
478
- const transport = new StdioServerTransport();
479
- await server.connect(transport);
480
- console.error("iMessage MCP Server running on stdio");