iMessage MCP
# iMessage MCP
A local [MCP](https://modelcontextprotocol.io) server that reads your macOS
Messages history and sends new iMessages. Everything runs locally — nothing
leaves your machine.
## Tools
| Tool | What it does |
| :--- | :--- |
| `list_chats` | Most recently active conversations (identifiers + last-message time). |
| `get_messages` | Recent messages from one conversation (by identifier or guid). |
| `get_recent_messages` | Newest messages across every conversation, with chat attribution. |
| `search_messages` | Substring search over message text. |
| `send_message` | Send an iMessage to a phone number or email. *(opt-in)* |
| `send_to_chat` | Send to an existing chat by guid, for group chats. *(opt-in)* |
Read tools are capped at 200 rows per call and exclude tapbacks. The two send
tools are **not registered at all** unless `IMESSAGE_MCP_ALLOW_SEND=1` is set,
so a host configured for reading cannot send even by accident.
## Security model
Reading your messages and sending as you are very different powers, and putting
both in one agent session is the risky part. Anyone who can text you can put
text into `get_recent_messages`, so message bodies are attacker-controlled
input sitting next to a tool that sends mail as you.
What this server does about that:
- Send tools are off by default and must be switched on per host.
- `IMESSAGE_MCP_ALLOWED_RECIPIENTS` restricts who can be messaged at all.
- Every send attempt and outcome is appended to a local audit log before and
after the AppleScript call, so a send can't happen without a record.
- Outgoing messages are length-capped and cannot be empty.
- The server advertises MCP `instructions` and per-tool annotations
(`readOnlyHint`, `destructiveHint`) so hosts can prompt on the dangerous ones.
What it does **not** do: it can't stop a model that reads a malicious message
from deciding to act on it. If you enable sending, keep an approval prompt on
the send tools.
## Configuration
All optional. Set them in your MCP host's config, not in your shell profile.
| Variable | Default | Meaning |
| :--- | :--- | :--- |
| `IMESSAGE_MCP_ALLOW_SEND` | off | `1` registers the two send tools. |
| `IMESSAGE_MCP_ALLOWED_RECIPIENTS` | empty | Comma-separated allowlist. Empty means no restriction. Phone formatting is normalized, so `+15551234567` and `(555) 123-4567` match. |
| `IMESSAGE_MCP_MAX_CHARS` | `2000` | Max outgoing message length. |
| `IMESSAGE_MCP_SEND_LOG` | `~/.imessage-mcp/sent.log` | JSON-lines audit log. |
| `IMESSAGE_MCP_DB` | `~/Library/Messages/chat.db` | Override the database path. |
## Setup
```bash
git clone https://github.com/tarun101/imessage-mcp.git
cd imessage-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
Python 3.10 or newer. Everything below assumes `$REPO` is the absolute path to
your clone.
> The `mcp` dependency is pinned to `<2`. The 2.x SDK removed
> `mcp.server.fastmcp`, which this server imports; an unpinned install picks up
> 2.x and fails at startup.
### macOS permissions (both required)
1. **Full Disk Access** — reading `~/Library/Messages/chat.db` is blocked
without it. Add whichever process runs the server (Terminal, iTerm, or your
MCP host app) under *System Settings → Privacy & Security → Full Disk
Access*, then fully quit and reopen it.
2. **Automation → Messages** — the first `send_message`/`send_to_chat` call
triggers a one-time permission prompt for controlling Messages.app. Allow it.
### Quick test
```bash
source .venv/bin/activate
python -c "import server; print(server.list_chats(5))"
```
### Tests
```bash
pip install -e '.[dev]'
pytest
```
The suite builds a synthetic `chat.db` with Apple's schema, so it runs anywhere
— no Mac and no access to your real message history required.
## Register with an MCP host
### Codex
```bash
codex mcp add imessage --env IMESSAGE_MCP_ALLOW_SEND=1 -- \
"$REPO/.venv/bin/python" "$REPO/server.py"
```
Or in `~/.codex/config.toml`, which also lets you require approval per tool:
```toml
[mcp_servers.imessage]
command = "/absolute/path/to/imessage-mcp/.venv/bin/python"
args = ["/absolute/path/to/imessage-mcp/server.py"]
default_tools_approval_mode = "auto"
[mcp_servers.imessage.env]
IMESSAGE_MCP_ALLOW_SEND = "1"
[mcp_servers.imessage.tools.send_message]
approval_mode = "prompt"
[mcp_servers.imessage.tools.send_to_chat]
approval_mode = "prompt"
```
Verify with `/mcp` inside the Codex TUI.
### Claude Code (CLI)
```bash
claude mcp add imessage -- "$REPO/.venv/bin/python" "$REPO/server.py"
```
### Claude Desktop / generic MCP config
```json
{
"mcpServers": {
"imessage": {
"command": "/absolute/path/to/imessage-mcp/.venv/bin/python",
"args": ["/absolute/path/to/imessage-mcp/server.py"]
}
}
}
```
Use absolute paths so the venv's Python (with `mcp` installed) is used.
## Notes & limitations
- **Read-only DB access.** chat.db is opened with `mode=ro`; the server never
writes to your message store. Sends go exclusively through Messages.app.
- **WAL fallback.** chat.db is a WAL database, and a read-only handle can't
create the `-shm` file it needs. When a direct open fails — which happens
when Messages.app isn't running — the server transparently reads a temporary
snapshot copy instead, refreshed whenever the source changes.
- **attributedBody.** Newer messages store their body in a binary
`attributedBody` blob rather than the `text` column. The reader decodes these
heuristically — it covers ordinary text messages but may miss rich content.
Because of this, `search_messages` only matches the plain `text` column.
- **Timestamps** are returned as ISO-8601 UTC.
- **Sends are verified, not assumed.** AppleScript returns before delivery, so
after sending the server polls chat.db for the outgoing row. A result of
`sent_unverified` means Messages.app accepted it but it hasn't landed yet —
usually lag rather than failure.
- **Sending is real.** These tools send actual messages that cannot be recalled.
TDQS
Scored across 4 tools
list_chats and search_messages are distinct, but get_messages and get_recent_messages have potentially confusingly similar purposes—both return recent messages newest first and differ mainly in scope (single conversation vs. all conversations). The descriptions clarify the difference, but an agent could easily pick the wrong one without careful reading.
All four tools consistently follow a clear snake_case verb_noun pattern (list_chats, get_messages, get_recent_messages, search_messages). The verbs are distinct and descriptive, with no mixing of conventions or styles.
Four tools is well-scoped for a read-only iMessage server: one for chat discovery, two for message retrieval (per-chat and global), and one for search. Each tool earns its place with no bloat.
The read surface is decent, but the server is called iMessage and list_chats explicitly references a send_to_chat tool that is absent, leaving a significant dead end for agents wanting to send messages. Search also admits to missing newer attributedBody messages, so coverage feels incomplete.