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

```sh
# 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`:

```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:

```sh
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:

```sh
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:

```toml
[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:

```sh
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`):

```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:

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

Tune the poll cadence:

```sh
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][upstream]).
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.

[upstream]: https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/imessage

## License

MIT OR Apache-2.0.