imessage-mcp-server 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/index.js +480 -0
  4. package/package.json +50 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tinyxia
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,228 @@
1
+ # imessage-mcp-server
2
+
3
+ > **MCP server for reading and sending iMessages on macOS**
4
+ > Expose your iMessage history and send capabilities via the [Model Context Protocol](https://modelcontextprotocol.io).
5
+
6
+ ⚠️ **macOS only** — requires the Messages app and its SQLite database (`~/Library/Messages/chat.db`).
7
+
8
+ ---
9
+
10
+ ## Features
11
+
12
+ This MCP server provides **4 tools** for interacting with iMessage:
13
+
14
+ | Tool | Description |
15
+ |------|-------------|
16
+ | `send_imessage` | Send an iMessage to a recipient's email or phone number |
17
+ | `list_conversations` | List recent conversations with latest message preview and unread count |
18
+ | `read_conversation` | Read paginated messages from a specific conversation (by chat_id or handle) |
19
+ | `get_new_messages` | Poll for new incoming messages since a timestamp |
20
+
21
+ ---
22
+
23
+ ## Prerequisites
24
+
25
+ - **macOS** (Ventura or later recommended)
26
+ - **Node.js >= 18**
27
+ - **iMessage signed in** via the Messages app
28
+ - **Full Disk Access** for your terminal / IDE
29
+ - Go to _System Settings → Privacy & Security → Full Disk Access_
30
+ - Add your terminal app (Terminal, iTerm2, VS Code, etc.)
31
+ - **Xcode Command Line Tools** (needed to compile the native `better-sqlite3` module)
32
+
33
+ ```bash
34
+ xcode-select --install
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Quick Start
40
+
41
+ ### Option 1: npx (recommended)
42
+
43
+ No installation needed — just add to your MCP configuration:
44
+
45
+ **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "imessage": {
51
+ "command": "npx",
52
+ "args": ["-y", "imessage-mcp-server"]
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ **Claude Code** (`.claude/settings.json` in your project):
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "imessage": {
64
+ "command": "npx",
65
+ "args": ["-y", "imessage-mcp-server"]
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ **Cursor** (`.cursor/mcp.json`):
72
+
73
+ ```json
74
+ {
75
+ "mcpServers": {
76
+ "imessage": {
77
+ "command": "npx",
78
+ "args": ["-y", "imessage-mcp-server"]
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ **VS Code + GitHub Copilot** (`.vscode/mcp.json`):
85
+
86
+ ```json
87
+ {
88
+ "servers": {
89
+ "imessage": {
90
+ "command": "npx",
91
+ "args": ["-y", "imessage-mcp-server"]
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ### Option 2: Global install
98
+
99
+ ```bash
100
+ npm install -g imessage-mcp-server
101
+ ```
102
+
103
+ Then reference the binary directly in your MCP config:
104
+
105
+ ```json
106
+ {
107
+ "mcpServers": {
108
+ "imessage": {
109
+ "command": "imessage-mcp",
110
+ "args": []
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ ### Option 3: Clone from source
117
+
118
+ ```bash
119
+ git clone https://github.com/tinyxia/imessage-mcp.git
120
+ cd imessage-mcp
121
+ npm install
122
+ ```
123
+
124
+ Then reference the local path:
125
+
126
+ ```json
127
+ {
128
+ "mcpServers": {
129
+ "imessage": {
130
+ "command": "node",
131
+ "args": ["/path/to/imessage-mcp/index.js"]
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Tool Reference
140
+
141
+ ### `send_imessage`
142
+
143
+ Send an iMessage to a recipient.
144
+
145
+ **Parameters:**
146
+
147
+ | Param | Type | Required | Description |
148
+ |-------|------|----------|-------------|
149
+ | `recipient` | string | ✅ | Email address or phone number of the recipient |
150
+ | `text` | string | ✅ | Message text to send |
151
+
152
+ **Example:**
153
+ ```json
154
+ { "recipient": "example@icloud.com", "text": "Hello from MCP!" }
155
+ ```
156
+
157
+ ### `list_conversations`
158
+
159
+ List recent conversations with the latest message preview and unread count.
160
+
161
+ **Parameters:**
162
+
163
+ | Param | Type | Required | Default | Description |
164
+ |-------|------|----------|---------|-------------|
165
+ | `limit` | number | ❌ | 20 | Max conversations to return (max 100) |
166
+
167
+ ### `read_conversation`
168
+
169
+ Read messages from a specific conversation.
170
+
171
+ **Parameters:**
172
+
173
+ | Param | Type | Required | Default | Description |
174
+ |-------|------|----------|---------|-------------|
175
+ | `handle` | string | ❌ | — | Filter by handle (email or phone number) |
176
+ | `chat_id` | number | ❌ | — | Filter by chat ROWID |
177
+ | `limit` | number | ❌ | 30 | Max messages to return (max 200) |
178
+ | `before_id` | number | ❌ | — | Paginate: return messages before this ROWID |
179
+ | `include_read` | boolean | ❌ | true | Include already-read messages |
180
+ | `unread_only` | boolean | ❌ | false | Only return unread messages |
181
+
182
+ ### `get_new_messages`
183
+
184
+ Poll for recently received messages.
185
+
186
+ **Parameters:**
187
+
188
+ | Param | Type | Required | Default | Description |
189
+ |-------|------|----------|---------|-------------|
190
+ | `since` | string | ❌ | last 10 | ISO timestamp to fetch messages from (e.g. `2026-06-28T10:00:00.000Z`) |
191
+ | `mark_read` | boolean | ❌ | false | Mark unread messages as read in chat.db |
192
+ | `max_results` | number | ❌ | 10 | Max messages to return (max 100) |
193
+
194
+ ---
195
+
196
+ ## Environment Variables
197
+
198
+ | Variable | Default | Description |
199
+ |----------|---------|-------------|
200
+ | `IMESSAGE_DB_PATH` | `~/Library/Messages/chat.db` | Override the path to the iMessage chat database |
201
+
202
+ ---
203
+
204
+ ## Troubleshooting
205
+
206
+ | Problem | Solution |
207
+ |---------|----------|
208
+ | ❌ **iMessage database not found** | Make sure you are signed into iMessage in the Messages app. If your database is in a non-standard location, set `IMESSAGE_DB_PATH`. |
209
+ | 🔒 **Permission denied / database unreadable** | Grant **Full Disk Access** to your terminal/IDE in _System Settings → Privacy & Security → Full Disk Access_. Restart your terminal after granting. |
210
+ | 🔐 **"Database is locked"** | Quit the Messages app completely or wait for sync to finish. |
211
+ | 💻 **osascript failed** | Make sure Messages.app is running and signed into your Apple ID. |
212
+ | ⚠️ **better-sqlite3 compilation errors** | Install Xcode CLI Tools: `xcode-select --install` and try again. |
213
+ | 🐢 **First `npx` run is slow** | npx downloads and compiles native modules on first run. Subsequent runs are cached. |
214
+
215
+ ---
216
+
217
+ ## Security Notes
218
+
219
+ - The iMessage database is opened in **read-only mode** (`chat.db` is never modified by read operations)
220
+ - The `mark_read` option in `get_new_messages` is the only operation that writes to the database
221
+ - Sending messages is done through **AppleScript** (`osascript`) which respects macOS privacy controls
222
+ - Your chat data **never leaves your machine** — all processing is local
223
+
224
+ ---
225
+
226
+ ## License
227
+
228
+ MIT © 2026 tinyxia
package/index.js ADDED
@@ -0,0 +1,480 @@
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");
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "imessage-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for reading and sending iMessages on macOS",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "imessage-mcp-server": "./index.js"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "preferUnplugged": true,
19
+ "scripts": {
20
+ "start": "node index.js",
21
+ "test": "echo \"No tests yet\" && exit 0"
22
+ },
23
+ "keywords": [
24
+ "imessage",
25
+ "mcp",
26
+ "mcp-server",
27
+ "model-context-protocol",
28
+ "macos",
29
+ "messages",
30
+ "icloud",
31
+ "apple"
32
+ ],
33
+ "author": "tinyxia",
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/tinyxia/imessage-mcp.git"
41
+ },
42
+ "homepage": "https://github.com/tinyxia/imessage-mcp#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/tinyxia/imessage-mcp/issues"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.29.0",
48
+ "better-sqlite3": "^12.11.1"
49
+ }
50
+ }