Text your coding agent. Get a reply back in Messages.
This repo ships dkdc-io-imessage, a Rust MCP server for macOS. Point Codex CLI, Claude
Code, or any stdio MCP client at one binary and it can:
- send iMessage with
reply(chat_id, text) - search your chats with
list_messages(query, limit) - fetch one message by GUID with
read_message(id) - auto-inject inbound iMessages into the LLM's session as channel events
Local allowlist only. Empty config fails closed.
crates.io · docs · install script
A text arrives. Codex runs cal. The calendar comes back as iMessage.
╭─────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ model: gpt-5.4 │
│ directory: /private/tmp │
│ permissions: YOLO mode │
╰─────────────────────────────────────────╯
› channel envelope
from: owner
ts: 2026-04-18T00:50:58Z
hi codex! run `cal` and text me the output. use the default
chat_id (no need to specify).
• Running cal locally, then I'll send the exact output to your
default iMessage chat.
• Ran cal
└ April 2026
Su Mo Tu We Th Fr Sa
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
• Sending the cal output to your default chat now.
• Called
└ imessage.reply({"text":" April 2026 \n..."})
{"chat_id": "any;-;+1XXXXXXXXXX", "sent": true}
• Sent this to your default chat.
This is the whole pitch: your agent can stay in the terminal while you stay on your phone.
Same binary. Same three tools. Same round-trip.
▐▛███▜▌ Claude Code v2.1.114
▝▜█████▛▘ Opus 4.7 (1M context) · Claude Max
▘▘ ▝▝ /private/tmp
Listening for channel messages from: server:imessage
← imessage · +1XXXXXXXXXX: hi claude! run `cal` and text me the output. use the
defaul…
⏺ Bash(cal)
⎿ April 2026
Su Mo Tu We Th Fr Sa
1 2 3 4
… +4 lines (ctrl+o to expand)
Called imessage (ctrl+o to expand)
⏺ Sent the cal output (April 2026) to your default chat.
- one binary:
cargo install dkdc-io-imessage - no framework lock-in: Codex CLI, Claude Code, or any stdio MCP client
- real Messages integration: send via AppleScript, read from
chat.db - live push: incoming iMessages auto-inject as channel notifications
- attachment-aware reads: inbound messages surface
image_pathfor any attachments so the LLM canReadthem directly - tight surface area: three tools, no extra daemon, no event bus
- fail closed: no allowlist means no access
# no rust? one line:
curl -LsSf https://dkdc.sh/imessage-mcp/install.sh | sh
# already have cargo:
cargo install dkdc-io-imessageThen:
- grant Full Disk Access to the host process that will run the binary
- populate
~/.config/dkdc-io/imessage/access.toml - register the MCP server with your LLM CLI
Full setup and config snippets for Codex and Claude Code live in the crate README.
Prefer the CLI over hand-editing config files.
Codex:
codex mcp add imessage -- dkdc-io-imessage --stdio
codex mcp listClaude Code:
claude mcp add imessage dkdc-io-imessage --stdio
claude mcp listThe server name (imessage) becomes the MCP namespace in the tool list:
imessage.reply, imessage.list_messages, imessage.read_message.
To enable inbound-message push:
- Codex: nothing extra for
codex mcp additself. That works on both upstream Codex and Cody's fork. The fork only adds the async channel watcher whenCODEX_CHANNEL_DIRis set. - Claude Code: add
--dangerously-load-development-channels server:imessageto theclaudeinvocation. This is an experimental flag. It opts the session into the channel surface sodkdc-io-imessage --watchcan pushnotifications/claude/channelevents that render as← imessage · <handle>: <text>.
Direct edit works too, for reference:
# ~/.codex/config.toml
[mcp_servers.imessage]
command = "dkdc-io-imessage"
args = ["--stdio"]// ~/.claude.json
{
"mcpServers": {
"imessage": {
"type": "stdio",
"command": "dkdc-io-imessage",
"args": ["--stdio"]
}
}
}Codex note: codex mcp add works on both upstream Codex and Cody's fork. The
fork at https://github.com/lostmygithubaccount/codex only adds async channel
support around CODEX_CHANNEL_DIR.
replyonly sends to allowlisted handles orself.chat_idlist_messagesandread_messagenever surface non-allowlisted chats- AppleScript runs with argv, not string interpolation
- empty allowlist fails closed by default
The anti-regression coverage lives in tests/injection.rs and
tests/stdio_smoke.rs. A live Claude parity round-trip test lives in
crates/imessage-mcp/tests/claude_parity.rs and is documented in
crates/imessage-mcp/tests/claude_parity.md.
Install the local hook gate first:
bin/setupThat configures core.hooksPath to use .githooks/pre-commit, which runs:
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspace
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspaceAnthropic shipped the original TypeScript/Bun iMessage MCP server for Claude
Code (anthropics/claude-plugins-official/external_plugins/imessage).
We first ported that shape directly. Then two correctness bugs surfaced:
typedstream parsing truncated messages above roughly 130 bytes, and the
echo-tracker could re-surface outbound replies as inbound messages. Those bugs
were fixed, then the project was rewritten in Rust for correctness, not speed.
The shape stayed the same: stdio MCP, chat.db reads, AppleScript send, local
allowlist.
Dual MIT OR Apache-2.0.

