@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 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
@@ -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
+ }