dkdc-io-imessage
An iMessage MCP server. Lets an LLM CLI (Codex CLI, Claude Code, or any JSON-RPC-over-stdio MCP client) read and send iMessages on macOS.
Three tools:
reply(chat_id, text)— send an iMessage.list_messages(query, limit)— search recent messages.read_message(id)— fetch one message by GUID.
Fail-closed by default: an empty allowlist makes every tool call error out with a pointer back to the config file.
Install
# no rust? one line:
|
# already have cargo:
Either way you end up with the dkdc-io-imessage binary on your $PATH. The
first script installs rustup if it isn't present, then runs cargo install.
macOS prerequisites
- Full Disk Access. The binary reads
~/Library/Messages/chat.db. Grant it to whatever you're launching the MCP server from (your terminal, Codex, Claude Code). System Settings -> Privacy & Security -> Full Disk Access. - Messages.app signed in. Sending goes through
osascript-> Messages, so the app has to be running and logged in to your Apple ID.
Allowlist
Edit ~/.config/dkdc-io/imessage/access.toml:
# Other handles the LLM can interact with via DMs. Must appear BEFORE [self].
= [
"friend@example.com",
]
# Your own chat GUID. Lets an LLM `reply` with no chat_id to text you.
# Copy it from chat.db (`SELECT guid FROM chat WHERE style = 45`) or look at
# the URL bar in Messages after selecting your "note to self" chat.
[]
= "iMessage;-;+15551234567"
= ["+15551234567", "you@icloud.com"]
Verify with:
Empty allowlist is intentional. Any tool call returns:
allowlist is empty. dkdc-io-imessage is fail-closed by default. Edit
~/.config/dkdc-io/imessage/access.toml to add `self.chat_id` and/or
`allow_from` handles, then retry.
Configure the client
Codex CLI
Preferred path:
This uses Cody's fork at
https://github.com/lostmygithubaccount/codex. Upstream OpenAI Codex does not
ship codex mcp add today.
Direct edit works too, for reference:
[]
= "dkdc-io-imessage"
= ["--stdio"]
You should see imessage in the MCP list, with reply, list_messages, and
read_message available on the next Codex start.
Claude Code
Preferred path:
Direct edit works too, for reference. Add to ~/.claude.json (or per-project
.mcp.json):
You should see imessage in the MCP list on the next Claude start.
Example prompts
After setup:
- "text myself 'build done'"
- "what did I text Friend today?"
- "read the last message from my note-to-self chat"
Automatic push mode
dkdc-io-imessage runs a background watcher by default when started as an MCP
server. Every new allowlisted inbound iMessage is pushed into the LLM session
the moment it lands in chat.db, so you can just text the Mac and the agent
reacts — no list_messages poll loop required.
How it surfaces:
- Claude Code / codex fork: the watcher emits a
notifications/claude/channelJSON-RPC notification over the same stdio transport the MCP server is already using. Claude Code renders it as a<channel source="imessage" ...>block; the codex fork forwards it through the TUI event path as a new inbound turn. - Codex filesystem channel: if
CODEX_CHANNEL_DIRis set, the watcher also drops a JSON envelope under$CODEX_CHANNEL_DIR/inbox/. That is the fork's other channel surface; useful when codex is driven without MCP.
The watcher respects the same allowlist as the tools. Non-allowlisted senders
are never pushed. Groups and SMS are dropped for now — only
service = 'iMessage' and chat.style = 45 (DM) rows flow through.
Disable explicitly:
# session-wide
Tune the poll cadence:
# default
Config
| Env var | Purpose |
|---|---|
DKDC_IO_ACCESS_FILE |
Override the allowlist TOML path. |
DKDC_IO_STATE_DIR |
Override the config dir (default ~/.config/dkdc-io/imessage). |
DKDC_IO_CHAT_DB |
Override the chat.db path. Useful for tests. |
DKDC_IO_LOG |
Tracing filter (warn, info, debug, ...). |
DKDC_IO_WATCH |
0/false/no disables the push watcher. |
DKDC_IO_WATCH_INTERVAL_MS |
Poll cadence in ms (default 750, min 100). |
CODEX_CHANNEL_DIR |
When set, also drop codex envelopes under <dir>/inbox/. |
Security posture
- Allowlist is the only access surface. Empty = fail closed.
replyrejects chat GUIDs that don't resolve through an allowlisted handle (orself.chat_id).read_message/list_messagesnever surface rows from non-allowlisted chats.- osascript is invoked with
text/chat_guidas argv items; the AppleScript body is a fixed string. There is no shell or string-concatenation path for user-controlled input. Seetests/injection.rsfor the anti-regression.
Prior art
Anthropic shipped the original TypeScript/Bun iMessage MCP server for Claude Code (anthropics/claude-plugins-official/external_plugins/imessage). We first ported that shape, then hit two correctness bugs: typedstream truncation on messages above roughly 130 bytes, and echo-tracker replay of outbound replies as inbound messages. Those bugs were fixed, then the project was rewritten in Rust for correctness, not speed. The current server keeps the same chat.db + AppleScript + allowlist shape with an LLM-CLI-agnostic surface.
License
MIT OR Apache-2.0.