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.
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/index.js +480 -0
- 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
|
+
}
|