@goforgeit/mcp-imessage 0.5.2
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 +50 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +321 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Forge
|
|
2
|
+
Copyright (c) 2025 Prometheus Group LLC. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
PROPRIETARY LICENSE
|
|
5
|
+
|
|
6
|
+
This software and associated documentation files (the "Software") are the
|
|
7
|
+
exclusive property of Prometheus Group LLC. The Software is protected by
|
|
8
|
+
copyright laws and international treaty provisions.
|
|
9
|
+
|
|
10
|
+
GRANT OF ACCESS
|
|
11
|
+
|
|
12
|
+
Access to this Software is granted solely for the purpose of evaluation,
|
|
13
|
+
testing, and providing feedback to Prometheus Group LLC. This access does
|
|
14
|
+
not constitute a transfer of ownership, license to use commercially, or
|
|
15
|
+
any other rights not expressly stated herein.
|
|
16
|
+
|
|
17
|
+
RESTRICTIONS
|
|
18
|
+
|
|
19
|
+
You may NOT:
|
|
20
|
+
- Use the Software for commercial purposes without explicit written permission
|
|
21
|
+
- Copy, modify, merge, publish, distribute, sublicense, or sell copies of the Software
|
|
22
|
+
- Reverse engineer, decompile, or disassemble the Software
|
|
23
|
+
- Remove or alter any proprietary notices, labels, or marks on the Software
|
|
24
|
+
- Claim ownership or authorship of the Software or any derivative works
|
|
25
|
+
|
|
26
|
+
FEEDBACK
|
|
27
|
+
|
|
28
|
+
Any feedback, suggestions, ideas, or improvements you provide regarding the
|
|
29
|
+
Software shall become the exclusive property of Prometheus Group LLC. You
|
|
30
|
+
hereby assign all rights, title, and interest in such feedback to Prometheus
|
|
31
|
+
Group LLC without any obligation of compensation or attribution.
|
|
32
|
+
|
|
33
|
+
NO WARRANTY
|
|
34
|
+
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
36
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
37
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
38
|
+
PROMETHEUS GROUP LLC BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
39
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
|
40
|
+
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
41
|
+
|
|
42
|
+
TERMINATION
|
|
43
|
+
|
|
44
|
+
This access may be terminated at any time by Prometheus Group LLC without
|
|
45
|
+
prior notice. Upon termination, you must destroy all copies of the Software
|
|
46
|
+
in your possession.
|
|
47
|
+
|
|
48
|
+
CONTACT
|
|
49
|
+
|
|
50
|
+
For licensing inquiries, please visit: https://goforgeit.com
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @goforgeit/mcp-imessage
|
|
5
|
+
*
|
|
6
|
+
* Forge MCP server for iMessage on macOS.
|
|
7
|
+
* Reads messages from ~/Library/Messages/chat.db via sqlite3 CLI,
|
|
8
|
+
* searches contacts and sends messages via AppleScript.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface IMessageMCPServer {
|
|
12
|
+
server: McpServer;
|
|
13
|
+
getRegisteredTools(): string[];
|
|
14
|
+
start(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
declare function runAppleScript(script: string): Promise<string>;
|
|
17
|
+
declare function runSqlite(dbPath: string, query: string): Promise<string>;
|
|
18
|
+
declare function normalizePhoneNumber(phone: string): string[];
|
|
19
|
+
declare function createIMessageMCPServer(): IMessageMCPServer;
|
|
20
|
+
|
|
21
|
+
export { type IMessageMCPServer, createIMessageMCPServer, normalizePhoneNumber, runAppleScript, runSqlite };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { execFile } from "child_process";
|
|
8
|
+
import { promisify } from "util";
|
|
9
|
+
var execFileAsync = promisify(execFile);
|
|
10
|
+
async function runAppleScript(script) {
|
|
11
|
+
const { stdout } = await execFileAsync("osascript", ["-e", script]);
|
|
12
|
+
return stdout.trim();
|
|
13
|
+
}
|
|
14
|
+
async function runSqlite(dbPath, query) {
|
|
15
|
+
const { stdout } = await execFileAsync("sqlite3", ["-json", dbPath, query]);
|
|
16
|
+
return stdout.trim();
|
|
17
|
+
}
|
|
18
|
+
function normalizePhoneNumber(phone) {
|
|
19
|
+
const cleaned = phone.replace(/[^0-9+]/g, "");
|
|
20
|
+
if (/^\+1\d{10}$/.test(cleaned)) return [cleaned];
|
|
21
|
+
if (/^1\d{10}$/.test(cleaned)) return [`+${cleaned}`];
|
|
22
|
+
if (/^\d{10}$/.test(cleaned)) return [`+1${cleaned}`];
|
|
23
|
+
const formats = /* @__PURE__ */ new Set();
|
|
24
|
+
if (cleaned.startsWith("+1")) {
|
|
25
|
+
formats.add(cleaned);
|
|
26
|
+
} else if (cleaned.startsWith("1")) {
|
|
27
|
+
formats.add(`+${cleaned}`);
|
|
28
|
+
} else {
|
|
29
|
+
formats.add(`+1${cleaned}`);
|
|
30
|
+
}
|
|
31
|
+
return Array.from(formats);
|
|
32
|
+
}
|
|
33
|
+
function getDbPath() {
|
|
34
|
+
return `${process.env.HOME}/Library/Messages/chat.db`;
|
|
35
|
+
}
|
|
36
|
+
function createIMessageMCPServer() {
|
|
37
|
+
const server = new McpServer({
|
|
38
|
+
name: "@goforgeit/mcp-imessage",
|
|
39
|
+
version: "0.1.0"
|
|
40
|
+
});
|
|
41
|
+
const registeredTools = [];
|
|
42
|
+
server.tool(
|
|
43
|
+
"search_contacts",
|
|
44
|
+
"Search contacts by name, phone number, or email address",
|
|
45
|
+
{
|
|
46
|
+
query: z.string().describe("Search query (name, phone, or email)")
|
|
47
|
+
},
|
|
48
|
+
async ({ query }) => {
|
|
49
|
+
const script = `
|
|
50
|
+
tell application "Contacts"
|
|
51
|
+
set output to "["
|
|
52
|
+
set isFirst to true
|
|
53
|
+
repeat with p in every person
|
|
54
|
+
if ((name of p as text) contains ${JSON.stringify(query)}) then
|
|
55
|
+
if not isFirst then
|
|
56
|
+
set output to output & ","
|
|
57
|
+
end if
|
|
58
|
+
set output to output & "{"
|
|
59
|
+
set output to output & "\\"name\\":\\"" & (name of p as text) & "\\","
|
|
60
|
+
set output to output & "\\"phones\\":["
|
|
61
|
+
set firstPhone to true
|
|
62
|
+
repeat with ph in phones of p
|
|
63
|
+
if not firstPhone then
|
|
64
|
+
set output to output & ","
|
|
65
|
+
end if
|
|
66
|
+
set output to output & "\\"" & (value of ph) & "\\""
|
|
67
|
+
set firstPhone to false
|
|
68
|
+
end repeat
|
|
69
|
+
set output to output & "],"
|
|
70
|
+
set output to output & "\\"emails\\":["
|
|
71
|
+
set firstEmail to true
|
|
72
|
+
repeat with em in emails of p
|
|
73
|
+
if not firstEmail then
|
|
74
|
+
set output to output & ","
|
|
75
|
+
end if
|
|
76
|
+
set output to output & "\\"" & (value of em) & "\\""
|
|
77
|
+
set firstEmail to false
|
|
78
|
+
end repeat
|
|
79
|
+
set output to output & "]"
|
|
80
|
+
set output to output & "}"
|
|
81
|
+
set isFirst to false
|
|
82
|
+
end if
|
|
83
|
+
end repeat
|
|
84
|
+
return output & "]"
|
|
85
|
+
end tell
|
|
86
|
+
`;
|
|
87
|
+
try {
|
|
88
|
+
const results = await runAppleScript(script);
|
|
89
|
+
return { content: [{ type: "text", text: results }] };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: `Search failed: ${error instanceof Error ? error.message : String(error)}`
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
isError: true
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
registeredTools.push("search_contacts");
|
|
104
|
+
server.tool(
|
|
105
|
+
"list_conversations",
|
|
106
|
+
"List recent iMessage conversations with contact info and last message preview",
|
|
107
|
+
{
|
|
108
|
+
limit: z.number().optional().default(20).describe("Maximum conversations to return")
|
|
109
|
+
},
|
|
110
|
+
async ({ limit }) => {
|
|
111
|
+
const dbPath = getDbPath();
|
|
112
|
+
const query = `
|
|
113
|
+
SELECT
|
|
114
|
+
c.ROWID as chat_id,
|
|
115
|
+
c.display_name,
|
|
116
|
+
c.chat_identifier,
|
|
117
|
+
h.id as handle_id,
|
|
118
|
+
(SELECT m2.text FROM message m2
|
|
119
|
+
JOIN chat_message_join cmj2 ON cmj2.message_id = m2.ROWID
|
|
120
|
+
WHERE cmj2.chat_id = c.ROWID
|
|
121
|
+
ORDER BY m2.date DESC LIMIT 1) as last_message,
|
|
122
|
+
(SELECT datetime(m3.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime')
|
|
123
|
+
FROM message m3
|
|
124
|
+
JOIN chat_message_join cmj3 ON cmj3.message_id = m3.ROWID
|
|
125
|
+
WHERE cmj3.chat_id = c.ROWID
|
|
126
|
+
ORDER BY m3.date DESC LIMIT 1) as last_date,
|
|
127
|
+
c.service_name
|
|
128
|
+
FROM chat c
|
|
129
|
+
LEFT JOIN chat_handle_join chj ON chj.chat_id = c.ROWID
|
|
130
|
+
LEFT JOIN handle h ON h.ROWID = chj.handle_id
|
|
131
|
+
GROUP BY c.ROWID
|
|
132
|
+
ORDER BY last_date DESC
|
|
133
|
+
LIMIT ${limit}
|
|
134
|
+
`;
|
|
135
|
+
try {
|
|
136
|
+
const stdout = await runSqlite(dbPath, query);
|
|
137
|
+
if (!stdout) {
|
|
138
|
+
return {
|
|
139
|
+
content: [{ type: "text", text: "No conversations found." }]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const conversations = JSON.parse(stdout);
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text", text: JSON.stringify(conversations, null, 2) }]
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: "text", text: `Failed to list conversations: ${msg}` }],
|
|
150
|
+
isError: true
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
registeredTools.push("list_conversations");
|
|
156
|
+
server.tool(
|
|
157
|
+
"read_imessages",
|
|
158
|
+
"Read recent iMessages from a specific contact by phone number or email",
|
|
159
|
+
{
|
|
160
|
+
phone_number: z.string().describe("Phone number or email of the contact"),
|
|
161
|
+
limit: z.number().optional().default(10).describe("Maximum messages to retrieve")
|
|
162
|
+
},
|
|
163
|
+
async ({ phone_number, limit }) => {
|
|
164
|
+
const dbPath = getDbPath();
|
|
165
|
+
const phoneFormats = normalizePhoneNumber(phone_number);
|
|
166
|
+
const placeholders = phoneFormats.map((f) => `'${f}'`).join(", ");
|
|
167
|
+
const query = `
|
|
168
|
+
SELECT
|
|
169
|
+
m.ROWID as message_id,
|
|
170
|
+
CASE
|
|
171
|
+
WHEN m.text IS NOT NULL AND m.text != '' THEN m.text
|
|
172
|
+
ELSE '[No text content]'
|
|
173
|
+
END as content,
|
|
174
|
+
datetime(m.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as date,
|
|
175
|
+
h.id as sender,
|
|
176
|
+
m.is_from_me,
|
|
177
|
+
m.cache_has_attachments
|
|
178
|
+
FROM message m
|
|
179
|
+
INNER JOIN handle h ON h.ROWID = m.handle_id
|
|
180
|
+
WHERE h.id IN (${placeholders})
|
|
181
|
+
AND (m.text IS NOT NULL OR m.cache_has_attachments = 1)
|
|
182
|
+
AND m.item_type = 0
|
|
183
|
+
AND m.is_audio_message = 0
|
|
184
|
+
ORDER BY m.date DESC
|
|
185
|
+
LIMIT ${limit}
|
|
186
|
+
`;
|
|
187
|
+
try {
|
|
188
|
+
const stdout = await runSqlite(dbPath, query);
|
|
189
|
+
if (!stdout) {
|
|
190
|
+
return {
|
|
191
|
+
content: [{ type: "text", text: "No messages found for this contact." }]
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const messages = JSON.parse(stdout);
|
|
195
|
+
return { content: [{ type: "text", text: JSON.stringify(messages, null, 2) }] };
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
198
|
+
if (msg.includes("unable to open database") || msg.includes("permission denied")) {
|
|
199
|
+
return {
|
|
200
|
+
content: [
|
|
201
|
+
{
|
|
202
|
+
type: "text",
|
|
203
|
+
text: "Cannot access Messages database. Grant Full Disk Access to your terminal/IDE:\n1. Open System Settings > Privacy & Security > Full Disk Access\n2. Enable your terminal app\n3. Restart the terminal"
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
content: [{ type: "text", text: `Failed to read messages: ${msg}` }],
|
|
210
|
+
isError: true
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
registeredTools.push("read_imessages");
|
|
216
|
+
server.tool(
|
|
217
|
+
"get_unread_imessages",
|
|
218
|
+
"Get all unread iMessages across all conversations",
|
|
219
|
+
{
|
|
220
|
+
limit: z.number().optional().default(10).describe("Maximum unread messages to retrieve")
|
|
221
|
+
},
|
|
222
|
+
async ({ limit }) => {
|
|
223
|
+
const dbPath = getDbPath();
|
|
224
|
+
const query = `
|
|
225
|
+
SELECT
|
|
226
|
+
m.ROWID as message_id,
|
|
227
|
+
CASE
|
|
228
|
+
WHEN m.text IS NOT NULL AND m.text != '' THEN m.text
|
|
229
|
+
ELSE '[No text content]'
|
|
230
|
+
END as content,
|
|
231
|
+
datetime(m.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as date,
|
|
232
|
+
h.id as sender,
|
|
233
|
+
m.is_from_me,
|
|
234
|
+
m.cache_has_attachments
|
|
235
|
+
FROM message m
|
|
236
|
+
INNER JOIN handle h ON h.ROWID = m.handle_id
|
|
237
|
+
WHERE m.is_from_me = 0
|
|
238
|
+
AND m.is_read = 0
|
|
239
|
+
AND (m.text IS NOT NULL OR m.cache_has_attachments = 1)
|
|
240
|
+
AND m.is_audio_message = 0
|
|
241
|
+
AND m.item_type = 0
|
|
242
|
+
ORDER BY m.date DESC
|
|
243
|
+
LIMIT ${limit}
|
|
244
|
+
`;
|
|
245
|
+
try {
|
|
246
|
+
const stdout = await runSqlite(dbPath, query);
|
|
247
|
+
if (!stdout) {
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: "text", text: "No unread messages." }]
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const messages = JSON.parse(stdout);
|
|
253
|
+
return { content: [{ type: "text", text: JSON.stringify(messages, null, 2) }] };
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
256
|
+
return {
|
|
257
|
+
content: [{ type: "text", text: `Failed to get unread messages: ${msg}` }],
|
|
258
|
+
isError: true
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
registeredTools.push("get_unread_imessages");
|
|
264
|
+
server.tool(
|
|
265
|
+
"send_imessage",
|
|
266
|
+
"Send an iMessage to a contact via the macOS Messages app",
|
|
267
|
+
{
|
|
268
|
+
recipient: z.string().describe("Phone number or email of the recipient"),
|
|
269
|
+
message: z.string().describe("Message content to send")
|
|
270
|
+
},
|
|
271
|
+
async ({ recipient, message }) => {
|
|
272
|
+
const script = `
|
|
273
|
+
tell application "Messages"
|
|
274
|
+
send ${JSON.stringify(message)} to buddy ${JSON.stringify(recipient)} of (service 1 whose service type = iMessage)
|
|
275
|
+
end tell
|
|
276
|
+
`;
|
|
277
|
+
try {
|
|
278
|
+
await runAppleScript(script);
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: "text", text: `Message sent successfully to ${recipient}` }]
|
|
281
|
+
};
|
|
282
|
+
} catch (error) {
|
|
283
|
+
return {
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: "text",
|
|
287
|
+
text: `Failed to send message: ${error instanceof Error ? error.message : String(error)}`
|
|
288
|
+
}
|
|
289
|
+
],
|
|
290
|
+
isError: true
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
registeredTools.push("send_imessage");
|
|
296
|
+
return {
|
|
297
|
+
server,
|
|
298
|
+
getRegisteredTools() {
|
|
299
|
+
return [...registeredTools];
|
|
300
|
+
},
|
|
301
|
+
async start() {
|
|
302
|
+
const transport = new StdioServerTransport();
|
|
303
|
+
await server.connect(transport);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
var isMainModule = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"));
|
|
308
|
+
if (isMainModule || process.env.FORGE_MCP_START === "true") {
|
|
309
|
+
const mcpServer = createIMessageMCPServer();
|
|
310
|
+
mcpServer.start().catch((err) => {
|
|
311
|
+
console.error("Failed to start iMessage MCP server:", err);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
export {
|
|
316
|
+
createIMessageMCPServer,
|
|
317
|
+
normalizePhoneNumber,
|
|
318
|
+
runAppleScript,
|
|
319
|
+
runSqlite
|
|
320
|
+
};
|
|
321
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @goforgeit/mcp-imessage\n *\n * Forge MCP server for iMessage on macOS.\n * Reads messages from ~/Library/Messages/chat.db via sqlite3 CLI,\n * searches contacts and sends messages via AppleScript.\n */\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod';\nimport { execFile } from 'child_process';\nimport { promisify } from 'util';\n\nconst execFileAsync = promisify(execFile);\n\nexport interface IMessageMCPServer {\n server: McpServer;\n getRegisteredTools(): string[];\n start(): Promise<void>;\n}\n\nexport async function runAppleScript(script: string): Promise<string> {\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n return stdout.trim();\n}\n\nexport async function runSqlite(dbPath: string, query: string): Promise<string> {\n const { stdout } = await execFileAsync('sqlite3', ['-json', dbPath, query]);\n return stdout.trim();\n}\n\nexport function normalizePhoneNumber(phone: string): string[] {\n const cleaned = phone.replace(/[^0-9+]/g, '');\n\n if (/^\\+1\\d{10}$/.test(cleaned)) return [cleaned];\n if (/^1\\d{10}$/.test(cleaned)) return [`+${cleaned}`];\n if (/^\\d{10}$/.test(cleaned)) return [`+1${cleaned}`];\n\n const formats = new Set<string>();\n if (cleaned.startsWith('+1')) {\n formats.add(cleaned);\n } else if (cleaned.startsWith('1')) {\n formats.add(`+${cleaned}`);\n } else {\n formats.add(`+1${cleaned}`);\n }\n return Array.from(formats);\n}\n\nfunction getDbPath(): string {\n return `${process.env.HOME}/Library/Messages/chat.db`;\n}\n\nexport function createIMessageMCPServer(): IMessageMCPServer {\n const server = new McpServer({\n name: '@goforgeit/mcp-imessage',\n version: '0.1.0',\n });\n\n const registeredTools: string[] = [];\n\n // --- search_contacts ---\n server.tool(\n 'search_contacts',\n 'Search contacts by name, phone number, or email address',\n {\n query: z.string().describe('Search query (name, phone, or email)'),\n },\n async ({ query }) => {\n const script = `\n tell application \"Contacts\"\n set output to \"[\"\n set isFirst to true\n repeat with p in every person\n if ((name of p as text) contains ${JSON.stringify(query)}) then\n if not isFirst then\n set output to output & \",\"\n end if\n set output to output & \"{\"\n set output to output & \"\\\\\"name\\\\\":\\\\\"\" & (name of p as text) & \"\\\\\",\"\n set output to output & \"\\\\\"phones\\\\\":[\"\n set firstPhone to true\n repeat with ph in phones of p\n if not firstPhone then\n set output to output & \",\"\n end if\n set output to output & \"\\\\\"\" & (value of ph) & \"\\\\\"\"\n set firstPhone to false\n end repeat\n set output to output & \"],\"\n set output to output & \"\\\\\"emails\\\\\":[\"\n set firstEmail to true\n repeat with em in emails of p\n if not firstEmail then\n set output to output & \",\"\n end if\n set output to output & \"\\\\\"\" & (value of em) & \"\\\\\"\"\n set firstEmail to false\n end repeat\n set output to output & \"]\"\n set output to output & \"}\"\n set isFirst to false\n end if\n end repeat\n return output & \"]\"\n end tell\n `;\n try {\n const results = await runAppleScript(script);\n return { content: [{ type: 'text' as const, text: results }] };\n } catch (error) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Search failed: ${error instanceof Error ? error.message : String(error)}`,\n },\n ],\n isError: true,\n };\n }\n },\n );\n registeredTools.push('search_contacts');\n\n // --- list_conversations ---\n server.tool(\n 'list_conversations',\n 'List recent iMessage conversations with contact info and last message preview',\n {\n limit: z.number().optional().default(20).describe('Maximum conversations to return'),\n },\n async ({ limit }) => {\n const dbPath = getDbPath();\n const query = `\n SELECT\n c.ROWID as chat_id,\n c.display_name,\n c.chat_identifier,\n h.id as handle_id,\n (SELECT m2.text FROM message m2\n JOIN chat_message_join cmj2 ON cmj2.message_id = m2.ROWID\n WHERE cmj2.chat_id = c.ROWID\n ORDER BY m2.date DESC LIMIT 1) as last_message,\n (SELECT datetime(m3.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime')\n FROM message m3\n JOIN chat_message_join cmj3 ON cmj3.message_id = m3.ROWID\n WHERE cmj3.chat_id = c.ROWID\n ORDER BY m3.date DESC LIMIT 1) as last_date,\n c.service_name\n FROM chat c\n LEFT JOIN chat_handle_join chj ON chj.chat_id = c.ROWID\n LEFT JOIN handle h ON h.ROWID = chj.handle_id\n GROUP BY c.ROWID\n ORDER BY last_date DESC\n LIMIT ${limit}\n `;\n\n try {\n const stdout = await runSqlite(dbPath, query);\n if (!stdout) {\n return {\n content: [{ type: 'text' as const, text: 'No conversations found.' }],\n };\n }\n const conversations = JSON.parse(stdout);\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(conversations, null, 2) }],\n };\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n return {\n content: [{ type: 'text' as const, text: `Failed to list conversations: ${msg}` }],\n isError: true,\n };\n }\n },\n );\n registeredTools.push('list_conversations');\n\n // --- read_imessages ---\n server.tool(\n 'read_imessages',\n 'Read recent iMessages from a specific contact by phone number or email',\n {\n phone_number: z.string().describe('Phone number or email of the contact'),\n limit: z.number().optional().default(10).describe('Maximum messages to retrieve'),\n },\n async ({ phone_number, limit }) => {\n const dbPath = getDbPath();\n const phoneFormats = normalizePhoneNumber(phone_number);\n const placeholders = phoneFormats.map((f) => `'${f}'`).join(', ');\n\n const query = `\n SELECT\n m.ROWID as message_id,\n CASE\n WHEN m.text IS NOT NULL AND m.text != '' THEN m.text\n ELSE '[No text content]'\n END as content,\n datetime(m.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as date,\n h.id as sender,\n m.is_from_me,\n m.cache_has_attachments\n FROM message m\n INNER JOIN handle h ON h.ROWID = m.handle_id\n WHERE h.id IN (${placeholders})\n AND (m.text IS NOT NULL OR m.cache_has_attachments = 1)\n AND m.item_type = 0\n AND m.is_audio_message = 0\n ORDER BY m.date DESC\n LIMIT ${limit}\n `;\n\n try {\n const stdout = await runSqlite(dbPath, query);\n if (!stdout) {\n return {\n content: [{ type: 'text' as const, text: 'No messages found for this contact.' }],\n };\n }\n const messages = JSON.parse(stdout);\n return { content: [{ type: 'text' as const, text: JSON.stringify(messages, null, 2) }] };\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n if (msg.includes('unable to open database') || msg.includes('permission denied')) {\n return {\n content: [\n {\n type: 'text' as const,\n text:\n 'Cannot access Messages database. Grant Full Disk Access to your terminal/IDE:\\n' +\n '1. Open System Settings > Privacy & Security > Full Disk Access\\n' +\n '2. Enable your terminal app\\n' +\n '3. Restart the terminal',\n },\n ],\n };\n }\n return {\n content: [{ type: 'text' as const, text: `Failed to read messages: ${msg}` }],\n isError: true,\n };\n }\n },\n );\n registeredTools.push('read_imessages');\n\n // --- get_unread_imessages ---\n server.tool(\n 'get_unread_imessages',\n 'Get all unread iMessages across all conversations',\n {\n limit: z.number().optional().default(10).describe('Maximum unread messages to retrieve'),\n },\n async ({ limit }) => {\n const dbPath = getDbPath();\n const query = `\n SELECT\n m.ROWID as message_id,\n CASE\n WHEN m.text IS NOT NULL AND m.text != '' THEN m.text\n ELSE '[No text content]'\n END as content,\n datetime(m.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as date,\n h.id as sender,\n m.is_from_me,\n m.cache_has_attachments\n FROM message m\n INNER JOIN handle h ON h.ROWID = m.handle_id\n WHERE m.is_from_me = 0\n AND m.is_read = 0\n AND (m.text IS NOT NULL OR m.cache_has_attachments = 1)\n AND m.is_audio_message = 0\n AND m.item_type = 0\n ORDER BY m.date DESC\n LIMIT ${limit}\n `;\n\n try {\n const stdout = await runSqlite(dbPath, query);\n if (!stdout) {\n return {\n content: [{ type: 'text' as const, text: 'No unread messages.' }],\n };\n }\n const messages = JSON.parse(stdout);\n return { content: [{ type: 'text' as const, text: JSON.stringify(messages, null, 2) }] };\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n return {\n content: [{ type: 'text' as const, text: `Failed to get unread messages: ${msg}` }],\n isError: true,\n };\n }\n },\n );\n registeredTools.push('get_unread_imessages');\n\n // --- send_imessage ---\n server.tool(\n 'send_imessage',\n 'Send an iMessage to a contact via the macOS Messages app',\n {\n recipient: z.string().describe('Phone number or email of the recipient'),\n message: z.string().describe('Message content to send'),\n },\n async ({ recipient, message }) => {\n const script = `\n tell application \"Messages\"\n send ${JSON.stringify(message)} to buddy ${JSON.stringify(recipient)} of (service 1 whose service type = iMessage)\n end tell\n `;\n try {\n await runAppleScript(script);\n return {\n content: [{ type: 'text' as const, text: `Message sent successfully to ${recipient}` }],\n };\n } catch (error) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Failed to send message: ${error instanceof Error ? error.message : String(error)}`,\n },\n ],\n isError: true,\n };\n }\n },\n );\n registeredTools.push('send_imessage');\n\n return {\n server,\n getRegisteredTools() {\n return [...registeredTools];\n },\n async start() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n },\n };\n}\n\n// CLI entry point\nconst isMainModule =\n process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\\\/g, '/'));\n\nif (isMainModule || process.env.FORGE_MCP_START === 'true') {\n const mcpServer = createIMessageMCPServer();\n mcpServer.start().catch((err) => {\n console.error('Failed to start iMessage MCP server:', err);\n process.exit(1);\n });\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goforgeit/mcp-imessage",
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Forge MCP server for iMessage — read, search, and send messages via macOS Messages app",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-imessage": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
20
|
+
"zod": "^4.2.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.10.2",
|
|
24
|
+
"tsup": "^8.0.0",
|
|
25
|
+
"vitest": "^3.2.4"
|
|
26
|
+
},
|
|
27
|
+
"os": [
|
|
28
|
+
"darwin"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run"
|
|
37
|
+
}
|
|
38
|
+
}
|