Skip to main content
Glama
dh-repo

Apple Messages MCP

by dh-repo
README.md
# apple-messages-mcp

Local MCP server that lets a Cursor or Grok Bot agent read **your** Messages.app database on the user's Mac (iMessage + SMS), and optionally send a reply through AppleScript.

There is no cloud inbox and no extra phone number. Reads stay on localhost. **Read-only by default.** **Scoped shut by default.** With no `MESSAGES_SCOPE` and no `MESSAGES_ALLOW_UNSCOPED=1`, list/search/thread/send/watch return `SCOPE`. A personal bot that wants the whole inbox sets one line: `MESSAGES_ALLOW_UNSCOPED=1`.

See [DESIGN.md](./DESIGN.md) for schema notes, the threat model, Phase 2 watcher hook, and agent instructions.

## Requirements

- macOS with Messages.app (the live `chat.db` path)
- Node.js 22 (engines: `>=22 <23`)
- Full Disk Access for the app that launches this server (Cursor, or Terminal if you test from a shell)
- Automation permission for Messages only if you turn send on

Linux CI uses a tiny fixture SQLite database. You do not need Messages.app to run `npm test`.

## Install (pinned 0.2.0)

This is meant to be cloned, built, and launched as `node /ABS/PATH/dist/index.js` — not `npx`'d unpinned from a random machine. Requires **Node 22**. `bin` is `dist/index.js`.

```bash
git clone https://github.com/dh-repo/apple-messages-mcp.git
cd apple-messages-mcp
# stay on a commit whose package.json version you intend to run (0.2.0+)
npm install
npm test
npm run build
node dist/index.js
```

The server speaks MCP over stdio (leave this to Cursor; do not write logs to stdout). The example Cursor config stays `node /ABS/PATH/TO/apple-messages-mcp/dist/index.js`.

Dev: `npx tsx src/index.ts`. Same binary is a local CLI for the smoke checklist and the Phase 2 watcher:

```bash
npx tsx src/index.ts status
npx tsx src/index.ts list
npx tsx src/index.ts thread --chat-id 12 --limit 20
npx tsx src/index.ts search snacks
npx tsx src/index.ts watch --interval 3000
```

## Full Disk Access

1. Open **System Settings → Privacy & Security → Full Disk Access**.
2. Enable **Cursor** (and **Terminal** if you smoke-test here).
3. Quit and reopen that app. TCC is applied at launch.
4. If `messages_status` returns `fda_likely_missing: true` or `PERMISSION`, this step is not done.

Without FDA, macOS returns `EPERM` for `~/Library/Messages/chat.db` even to the file's owner.

## Cursor / Claude Desktop config

Cursor **Add MCP server** / `mcp.json` (same shape):

```json
{
  "mcpServers": {
    "apple-messages": {
      "command": "node",
      "args": ["/ABS/PATH/TO/apple-messages-mcp/dist/index.js"],
      "env": {
        "MESSAGES_DB_MODE": "copy",
        "ENABLE_SEND": "0",
        "REDACT_PREVIEWS": "0"
      }
    }
  }
}
```

A checked-in copy lives at [examples/cursor-mcp.json](./examples/cursor-mcp.json). Point `args` at your clone.

Claude Desktop: paste the same `mcpServers` block into `claude_desktop_config.json`.

Dev without a build: `npx tsx src/index.ts`.

### Environment

Default is scoped shut. Pick one:

```json
"MESSAGES_SCOPE": "Family, 12"
```

```json
"MESSAGES_ALLOW_UNSCOPED": "1"
```

| Variable | Default | Purpose |
| --- | --- | --- |
| `MESSAGES_SCOPE` | unset | Comma/semicolon tokens: display name, `chat_id`, `guid`, handle. |
| `MESSAGES_ALLOW_UNSCOPED` | `0` | `1` is the only way to open the whole inbox. |
| `MESSAGES_DB_PATH` | `~/Library/Messages/chat.db` | Override for fixtures. |
| `MESSAGES_DB_MODE` | `copy` | Copy + WAL replay. `direct` opens the live file. |
| `ENABLE_SEND` | `0` | `1` enables `messages_send` (real texts). |
| `REDACT_PREVIEWS` | `0` | Bodies become `[redacted N chars]`. |
| `MESSAGES_WAKE_HOOK` | unset | Watcher runs this executable with one JSON event on stdin. |
| `WATCH_INCLUDE_PREVIEW` | `0` | Watcher includes a **redacted** preview. |

## Tools

| Tool | What it does |
| --- | --- |
| `messages_status` | Can we read `chat.db`? FDA missing? Scoped shut or unscoped? Snapshot size/age? |
| `messages_list_chats` | Recent chats, or only the allowlist if one is set. |
| `messages_get_thread` | Messages for a `chat_id` / guid / handle (required unless the allowlist is exactly one chat). |
| `messages_search` | Two-phase substring search (plain `text` LIKE, then a bounded Tahoe decode). Uses a sidecar of `(message_id, decoded_text)` so old blobs stay findable after they leave the scan window. Returns `truncated` when a row has not been decoded. |
| `messages_send` | Registered only when `ENABLE_SEND=1`. `dry_run: true` returns `{ to, chat_id, guid, body }` and does not call osascript. A real send still requires `confirm: true`. |

IDs in responses are SQLite `ROWID`s: `chat_id`, `message_id`, `handle_id`, `attachment_id`. Handles are phone numbers and emails, not Contacts names.

Copy this into the agent's MCP instructions: the block in [DESIGN.md §8](./DESIGN.md#8-what-grok-bot--cursor-agents-should-set-as-mcp-instructions).

## Watcher (Phase 2)

Not push, and not part of the MCP stdio stream. A second process polls `chat.db` (and optionally `fs.watch`s the WAL) for new ROWIDs — all chats, or only the allowlist if one is set — then writes JSON lines:

```bash
npx tsx src/index.ts watch --interval 3000
# {"type":"messages.ready","chat_id":…,"newest_message_id":…}
# {"type":"messages.new","chat_id":…,"count_new":1,"preview":null}
```

Pipe that into your own wake script, or set `MESSAGES_WAKE_HOOK` to [examples/wake-hook.sh](./examples/wake-hook.sh). The host should start an agent turn and call `messages_get_thread`. Do not treat this as a Slack-style channel.

## Smoke-test checklist (Mac)

1. `npm test` passes (fixture-only; safe on any machine).
2. Grant FDA, restart Cursor, reload the MCP server.
3. Call `messages_status`.
   - `readable: true`
   - `fda_likely_missing: false`
   - `unscoped: false` unless you set `MESSAGES_ALLOW_UNSCOPED=1`
   - Optional: set `MESSAGES_SCOPE` and confirm `scope.active: true` and `scope.chats` matches
4. `messages_list_chats` without scope env returns `SCOPE` and `scope.candidates`.
5. `messages_get_thread` with a `chat_id` from a scoped or unscoped list returns recent lines. At least some `text_source` values may be `attributedBody` on current macOS. `text_source: "guess"` means the decoder fell back to printable bytes.
6. `messages_search` with a word you know exists returns hits across chats, or empty + `truncated: true` when the Tahoe window missed.
7. `messages_send` is absent from `tools/list` while `ENABLE_SEND` is unset.
8. Optional: `ENABLE_SEND=1`, Automation allowed. `dry_run: true` returns `{ to, chat_id, guid, body }` without sending. A real send still needs `confirm: true`. Leave send off afterward.
9. `npx tsx src/index.ts status` (CLI) matches the MCP `messages_status` payload.
10. `npx tsx src/index.ts watch --interval 2000` prints one `messages.ready` line and, on a new message, `messages.new` without plaintext.

Inspector, if you want a GUI without Cursor:

```bash
npx @modelcontextprotocol/inspector npx tsx src/index.ts
```

## What this is not

- Not Inkbox / Linq / a hosted agent number.
- Not a push notification service. The Phase 2 `watch` process polls / `fs.watch`s `chat.db` and wakes the host — see DESIGN.md.
- Not a way to dump `chat.db` to a server.

## License

MIT. See [LICENSE](./LICENSE).

TDQS

A3.8/5.0

Scored across 4 tools

Disambiguation4/5

Each tool has a distinct role: messages_status is diagnostic, messages_list_chats enumerates conversations, messages_get_thread reads a single thread, and messages_search finds text. Minor overlap exists since list_chats returns a last-message preview and get_thread/search both return message content, but the intended usage is clear enough to avoid misselection.

Naming Consistency4/5

All tools share the messages_ prefix and mostly follow a verb_noun pattern (list_chats, get_thread, search). messages_status is a noun-only outlier, and messages_search lacks a noun object, but the prefix convention is consistent and readable.

Tool Count4/5

Four tools is on the lean side but reasonable for a focused, read-oriented Messages server covering diagnostics, listing, reading, and searching. It is slightly thin given lifecycle operations appear to exist elsewhere.

Completeness3/5

The surface covers discovery, read, and search but exposes no send/redact operations, even though messages_status references send/redact flags and descriptions mention send/watch returning SCOPE. These references imply tools that are not present, leaving a notable gap for a messaging domain.

Maintenance

ActivityMaintained
ResponsivenessNo issues