dkdc-io-imessage 0.2.2

iMessage MCP server for Codex CLI and Claude Code. Lets an LLM read and send iMessage on macOS.
Documentation

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:
curl -LsSf https://dkdc.sh/imessage-mcp/install.sh | sh

# already have cargo:
cargo install dkdc-io-imessage

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

  1. 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.
  2. 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].
allow_from = [
  "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.
[self]
chat_id  = "iMessage;-;+15551234567"
handles  = ["+15551234567", "you@icloud.com"]

Verify with:

dkdc-io-imessage check

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:

codex mcp add imessage -- dkdc-io-imessage --stdio
codex mcp list

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:

[mcp_servers.imessage]
command = "dkdc-io-imessage"
args    = ["--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:

claude mcp add imessage dkdc-io-imessage --stdio
claude mcp list

Direct edit works too, for reference. Add to ~/.claude.json (or per-project .mcp.json):

{
  "mcpServers": {
    "imessage": {
      "type": "stdio",
      "command": "dkdc-io-imessage",
      "args": ["--stdio"]
    }
  }
}

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/channel JSON-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_DIR is 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:

dkdc-io-imessage --stdio --no-watch       # one shot
export DKDC_IO_WATCH=0                    # session-wide

Tune the poll cadence:

export DKDC_IO_WATCH_INTERVAL_MS=750       # 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.
  • reply rejects chat GUIDs that don't resolve through an allowlisted handle (or self.chat_id).
  • read_message / list_messages never surface rows from non-allowlisted chats.
  • osascript is invoked with text / chat_guid as argv items; the AppleScript body is a fixed string. There is no shell or string-concatenation path for user-controlled input. See tests/injection.rs for 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.