Local MCP server for macOS Messages + Contacts: send messages, read chat history, wait for a reply without polling manually, and look up/update Contacts.app entries.
Runs over stdio, driven directly by whatever agent/client launches it (e.g. Claude Code, Claude Desktop). Only tested on and intended for macOS.
cd imessage-mcp
python3 -m venv .venv
.venv/bin/pip install -r requirements.txtThe process that actually runs this server (not your terminal, unless that's what launches it) needs, under System Settings > Privacy & Security:
- Full Disk Access — to read
~/Library/Messages/chat.dbdirectly. - Automation — to control Messages.app and Contacts.app via
osascript.
macOS prompts for these the first time each permission is needed; if a prompt doesn't appear, add the process manually in System Settings. If you're running this via Claude Code/Desktop, that's typically the app itself (or the terminal it was launched from) that needs the grant, not the Python interpreter.
claude mcp add imessage-mcp -- bash -c "cd /Users/john/mcp/imessage-mcp && .venv/bin/python -m imessage_mcp.server"The cd wrapper is required: this must run as python -m imessage_mcp.server
(a package, for the relative imports between modules) rather than by direct
script path, and -m only finds the package if the process's working
directory is the project root -- which isn't guaranteed to be true when
Claude Code launches it from wherever you happen to be working.
Use -s user instead of the default local scope if you want it available
across all projects, not just this directory.
| Tool | What it does |
|---|---|
imessage_send_message |
Send to a phone/email/group chat; tries iMessage, falls back to SMS/RCS, verifies delivery. |
imessage_list_chats |
List recent conversations with previews. |
imessage_list_messages |
Paginated message history for one chat, with per-message sender (handle + resolved contact name) and the group's participant roster. |
imessage_wait_for_reply |
Block (up to 30 min) until a new incoming message arrives — replaces hand-rolled polling loops. |
imessage_search_contacts |
Search Contacts.app by name. |
imessage_get_contact |
Full details (phones/emails/addresses) for one contact id. |
imessage_create_contact |
Create a new Contacts.app card. |
imessage_update_contact_address |
Add/replace an address by label (home/work/other). |
imessage_update_contact_phone |
Add/replace a phone by label (home/work/mobile/main/other). |
- Contacts.app duplicate cards: it's common for the same real person to
have 2+ separate
personrecords.imessage_search_contactsreturns all of them; keeping a contact's info consistent may mean calling the update tools once per duplicate id. - Contact ids can go stale: with iCloud Contacts sync on, a person's
idcan change shortly after it's created or edited (sync reassigns/ merges the record). If an update call errors saying the contact wasn't found, re-runimessage_search_contactsto get the current id and retry. - No delete/merge tools: intentionally out of scope, since this is meant to be low-risk to run against real data.
imessage_send_messagegroup chat support: only works for an existing group chat identifier; it won't create a new group.- Requires macOS Messages/Contacts apps to be installed and signed in; no support for other platforms.
- Message dates in
chat.dbare nanoseconds since 2001-01-01 (Apple's Core Data epoch);dates.pyhandles the conversion to/from ISO8601. - Structured Contacts.app output is parsed out of AppleScript using ASCII
separator control characters (record/group/unit/field separators)
generated at runtime via
(ASCII character N), rather than hand-built JSON in AppleScript. These are generated at runtime rather than embedded as literal bytes in the script source, because Python'sstr.splitlines()(used to break a script into-earguments) treats\x1c/\x1d/\x1eas line boundaries and would silently corrupt them. - All dynamic values (search queries, addresses, phone numbers) are passed
as
argvitems toosascript, never interpolated into the script source, to avoid AppleScript injection. - Sender identity in
imessage_list_messagescomes from thehandletable (message.handle_id→handle.id); the raw handle is then reverse-resolved to a Contacts.app name by dumping the address book once (bulk AppleScript accessors, cached ~60s) and matching phones on their trailing 10 digits. Name resolution needs Automation permission; if that's missing the read still succeeds and falls back to the raw handle.