# imessage-max [Health: Active]

**Category:** 💻 Developer Tools  
**Repository:** https://github.com/cyberpapiii/imessage-max  
**GitHub Stars:** 6  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/imessage-max

## Description
AI-optimized MCP server for iMessage on macOS. Read, search, and send messages.

## Claude Desktop Quick Installation
Heuristic fallback — verify the package name and runner against the repository README before running it. Uses `npx` (confidence: low):

```json
"mcpServers": {
  "imessage-max": {
    "command": "npx",
    "args": ["-y","imessage-max"]
  }
}
```

## Documentation & README

<p align="center">
  <img src="https://raw.githubusercontent.com/cyberpapiii/imessage-max/HEAD/icon.png" alt="iMessage Max" width="128">
</p>

# iMessage Max

An MCP (Model Context Protocol) server for iMessage. It lets AI agents read, search, and send your messages, resolving phone numbers to contact names along the way.

Written in Swift. One binary, no runtime dependencies.

## Distribution status

The project now ships a single Swift implementation:
- GitHub releases
- Homebrew
- source builds
- Codex plugin metadata
- Claude Desktop MCPB metadata

The old Python package has been retired and removed from the repository.
Everything current lives under `swift/`.

## Features

- 13 tools shaped around questions people actually ask, not around database tables
- Phone numbers resolve to names through macOS Contacts
- Three image variants: vision (1568px), thumb (400px), full (original), so a photo does not have to arrive at full size
- Messages group into sessions, split on gaps of 4 hours or more
- Attachment listings say whether each file is on disk or offloaded to iCloud
- Raw SQLite3 and Core Image GPU acceleration
- Reads chat.db only. Sending needs Automation permission for Messages.app

## Why this exists

Most iMessage tools expose raw database structures, requiring 3-5 tool calls per user intent. This MCP provides intent-aligned tools:

```
"What did Contact A and I talk about yesterday?"
→ find_chat(participants=["Contact A"]) + get_messages(since="yesterday")

"Show me the exact details for this thread before I reply"
→ get_chat_details(chat_id="chat123")

"Show me photos from the group chat"
→ list_attachments(chat_id="chat123", type="image")

"Find where we discussed the launch timeline"
→ search(query="launch timeline")
```

## Common agent workflows

The tools work best when an agent uses them as short workflows instead of isolated one-off calls.

Agents should treat `chat_id` values like `chat123` as internal handles for tool calls and exact sends. When explaining results to a person, use the returned chat name, group name, or participant-derived label instead of saying "Chat 123."

### Find the right conversation, then read it

```text
find_chat(participants=["Contact A"])
get_chat_details(chat_id="chat123")
get_messages(chat_id="chat123", since="yesterday", limit=50)
```

Use this when the person matters more than the exact thread id.

### Search first, then zoom in

```text
search(query="launch timeline", limit=10)
get_context(message_id="msg_456", before=5, after=10)
```

Use this when you know the topic but not where it was discussed.

### Check what needs attention

```text
get_unread()
get_active_conversations(hours=24, min_exchanges=2)
```

Use this to surface unread threads and active conversations after a broad chat-list sweep.

### Work with attachments safely

```text
list_attachments(chat_id="chat123", type="image", since="30d")
get_attachment(attachment_id="att123", variant="vision")
```

Use `list_attachments` to find the message where files were shared. It returns exact attachment ids and says whether each file is on disk, so you know before you fetch.

### Send with exact targeting when it matters

```text
find_chat(participants=["Contact A", "Contact B"])
send(chat_id="chat456", text="Please use the latest draft")
```

For sensitive sends, prefer resolving the exact chat first and then using `chat_id` so the message lands in the intended thread.

## Installation

### Homebrew (recommended)

```bash
brew tap cyberpapiii/tap
brew install imessage-max
```

### From source

```bash
git clone https://github.com/cyberpapiii/imessage-max.git
cd imessage-max/swift
swift build -c release

# Binary is at .build/release/imessage-max
```

For local development, advanced setup, and the signed install workflow, see:

- [swift/README.md](https://github.com/cyberpapiii/imessage-max/blob/HEAD/swift/README.md)

## Protocol support

iMessage Max is a dual-era MCP server. Both transports (stdio and HTTP)
serve both eras concurrently, selected per request:

| Era | Revisions | Lifecycle | Selected by |
| --- | --- | --- | --- |
| Modern | `2026-07-28` | Stateless, per-request `_meta` | `io.modelcontextprotocol/protocolVersion` in the request `_meta` (or `server/discover`) |
| Legacy | `2025-03-26` … `2025-11-25` | `initialize` + session | `initialize` request / `Mcp-Session-Id` |

Modern clients probe with `server/discover` and send the required
`MCP-Protocol-Version`, `Mcp-Method`, and (for `tools/call`) `Mcp-Name`
headers over HTTP. Legacy clients keep working unchanged. No client
migration is required, and legacy support stays as long as real clients
depend on it.

The server implements tools only. It has no prompts, resources,
completion, subscriptions, tasks, or MRTR flows, on purpose. The official
conformance suite runs against both eras with the documented baseline in
`docs/conformance-baseline.yml`.

## Client icon metadata

iMessage Max ships icons for the main MCP protocol surface and the client
packaging surfaces that use their own metadata:

- MCP `2025-11-25` initialize responses include PNG `serverInfo.icons`.
- Each tool advertises a compact PNG tool icon.
- Codex plugin metadata lives in `.codex-plugin/plugin.json` and uses
  `assets/codex/icon.png` plus `assets/codex/logo.png`.
- Claude Desktop / MCPB metadata lives in `mcpb/manifest.json` and uses PNG
  assets under `mcpb/assets/`.

The committed PNG source set is under `assets/icons/` at `16x16`, `32x32`,
`64x64`, `128x128`, `256x256`, and `512x512`.

## Setup

### 1. Grant Full Disk Access

Required to read `~/Library/Messages/chat.db`:

1. Open System Settings → Privacy & Security → Full Disk Access
2. Click + to add the binary

For Homebrew installs, the binary is at `/opt/homebrew/Cellar/imessage-max/VERSION/bin/imessage-max` (not the symlink at `/opt/homebrew/bin/`). Find it with:
```bash
# Open the folder containing the actual binary
open $(dirname $(readlink -f $(which imessage-max)))
```

For source builds, add `.build/release/imessage-max` from your clone directory.
After changing the grant, relaunch the server; macOS applies Full Disk Access only to processes started after the change.

> In the file picker, press ⌘+Shift+G and paste the path to go straight there.

### 2. Grant Contacts access

Required to resolve phone numbers to names. The server only asks for access when it is started from a terminal; launchd and MCP clients start it headless, and a headless process never prompts. Grant access once with `imessage-max --request-contacts-access` from a terminal, then restart the service. `--contacts-policy request|skip` (or `IMESSAGE_MAX_CONTACTS_POLICY`) overrides the terminal detection.

System Settings → Privacy & Security → Contacts → add `imessage-max` is the manual alternative.

### 3. Configure your MCP client

Add `imessage-max` to your MCP client's server configuration.

Many MCP clients use a JSON structure like this:

For Homebrew:
```json
{
  "mcpServers": {
    "imessage": {
      "command": "/opt/homebrew/Cellar/imessage-max/VERSION/bin/imessage-max"
    }
  }
}
```

For source builds:
```json
{
  "mcpServers": {
    "imessage": {
      "command": "/path/to/imessage-max/swift/.build/release/imessage-max"
    }
  }
}
```

If your client uses a different config format, point it at the same binary path.

### 4. Reconnect your MCP client

After saving the config, reconnect or restart your MCP client. The server should appear in the available tools, and you can verify the connection with `diagnose`.

## Tools

### find_chat
Find chats by participants, name, or recent content.

By default, chats Messages.app has filtered into Unknown Senders or Junk are hidden. The response carries `filtered_hidden`, the number of chats the filter removed from this view; pass `include_filtered=True` to see them.

```text
find_chat(participants=["Contact A"])              # Find a direct chat
find_chat(participants=["Contact A", "Contact B"]) # Find a group with both
find_chat(name="Project Group")                    # Find by chat name
find_chat(contains_recent="latest draft")          # Find by recent content
find_chat(name="Project Group", include_filtered=True)  # Also search junk / unknown-sender chats
```

### get_chat_details
Inspect a known thread without opening the full conversation.

```text
get_chat_details(chat_id="chat123")                          # Participants, handles, state, last message
get_chat_details(chat_id="chat123", include_shared_summary=false) # Skip recent shared summary
```

### get_messages
Retrieve messages with flexible filtering. Returns metadata for media. Explicit `chat_id` lookups are never filtered.

```text
get_messages(chat_id="chat123", limit=50)           # Recent messages
get_messages(chat_id="chat123", since="24h")        # Last 24 hours
get_messages(chat_id="chat123", from_person="Contact A")  # From specific person
get_messages(chat_id="chat123", has="links")        # Messages that contain a URL
```

`has` filters by content type (`links`, `attachments`, `images`). `links` includes link messages Messages stores as URL preview balloons (the common case on macOS 26).

Group system messages (renames, members added or removed, someone
leaving) come back with `text: null` and an `event` object:
`{"type": "rename", "title": "Trip"}`, `{"type": "participant_added", "participant": "Alice"}`,
`{"type": "participant_removed", ...}`, `{"type": "left"}`, or
`{"type": "other", "item_type": N}` for event kinds the server does not
name. Chat previews describe the same events in words
("renamed the group to Trip").

Reactions, replies, edits: each message may include `reactions`
(`["❤️ alice"]` for standard tapbacks; custom emoji show the emoji
itself; sticker reactions use the token `🩵 sticker`), `reply_to`
(`msg_<rowid>` of the originator), `reply_count`, and `edited: true`.
Removed tapbacks are omitted. All four fields are optional and omitted
when empty. `search` and `get_context` carry the same four fields.
These are read-only; `diagnose` still reports `tapbacks` and
`edit_unsend` as `unsupported` because the server cannot send them.

### get_messages_since
New messages across all chats after a ROWID cursor, in arrival order. Pass the returned `next_rowid` back as `since_rowid` to page or poll. `next_rowid` may be larger than the last returned message's rowid because consumed rows (reactions, filtered chats, orphans) advance it. Omit `since_rowid` to get only the current cursor. Cursors are only valid against this Mac's `chat.db`.

```text
get_messages_since()                          # Current cursor only
get_messages_since(since_rowid=234000)        # Messages after that row
get_messages_since(since_rowid=234000, limit=50)
get_messages_since(since_rowid=234000, chat_id="chat123")
get_messages_since(since_rowid=234000, include_filtered=True)
```

| Parameter | Type | Default | Meaning |
|-----------|------|---------|---------|
| `since_rowid` | integer | omitted | Exclusive ROWID cursor. Omit or pass `-1` for the current cursor only. |
| `chat_id` | string | omitted | Restrict to one chat (`chat123` or `123`) |
| `limit` | integer | 100 | Maximum messages to return (1–500) |
| `include_filtered` | boolean | false | Include junk / unknown-sender chats |
| `include_reactions` | boolean | true | Attach reaction strings to returned messages |

Example response:

```json
{
  "since_rowid": 234000,
  "messages": [
    {
      "id": "msg_234001",
      "rowid": 234001,
      "chat": {"id": "chat12", "name": "Alice Smith"},
      "from": "Alice Smith",
      "text": "on my way",
      "ts": "2026-09-02T15:00:00Z"
    },
    {
      "id": "msg_234010",
      "rowid": 234010,
      "chat": {"id": "chat40", "name": "Weekend"},
      "from": "me",
      "text": "see you there",
      "ts": "2026-09-02T15:01:00Z"
    }
  ],
  "next_rowid": 234050,
  "has_more": false,
  "current_rowid": 234050,
  "stalled": false,
  "filtered_hidden": 1
}
```

Polling recipe: call once without `since_rowid`, store `next_rowid`, then call with it on whatever cadence. When `stalled` is true, wait about one second and retry with the same cursor. Never compare `next_rowid` to message ids.

### get_attachment
Retrieve image content by attachment ID with resolution variants.

```text
get_attachment(attachment_id="att123")                 # Default: vision (1568px)
get_attachment(attachment_id="att123", variant="thumb") # Quick preview (400px)
get_attachment(attachment_id="att123", variant="full")  # Original resolution
```

| Variant | Resolution | Use Case | Token Cost |
|---------|------------|----------|------------|
| `vision` (default) | 1568px | AI analysis, OCR | ~1,600 tokens |
| `thumb` | 400px | Quick preview | ~200 tokens |
| `full` | Original | Maximum detail | Varies |

### list_chats
Browse recent chats with previews.

By default, chats Messages.app has filtered into Unknown Senders or Junk are hidden. The response carries `filtered_hidden`, the number of chats the filter removed; pass `include_filtered=True` to see them.

```text
list_chats(limit=20)          # Recent chats
list_chats(is_group=True)     # Only group chats
list_chats(since="7d")        # Active in last week
list_chats(include_filtered=True)   # Also show junk / unknown-sender chats
```

### search
Full-text search across messages.

By default, junk and unknown-sender chats are hidden. The response carries `filtered_hidden`; pass `include_filtered=True` to search them too.

```text
search(query="draft")                           # Search all messages
search(query="budget", from_person="Contact A") # From specific person
search(query="launch", is_group=True)           # Only in group chats
search(query="draft", include_filtered=True)    # Also search junk / unknown-sender chats
```

### get_context
Get messages surrounding a specific message.

```text
get_context(message_id="msg_123", before=5, after=10)
```

### get_active_conversations
Find chats with recent back-and-forth activity.

```text
get_active_conversations(hours=24)
get_active_conversations(is_group=True, min_exchanges=3)
```

### list_attachments
Browse shared items grouped by message. Each row includes exact attachment ids for follow-up fetches.

```text
list_attachments(type="image", since="7d")
list_attachments(chat_id="chat123", type="any")
```

Attachments Messages hides (`hide_attachment`, e.g. link-preview payloads) are not listed; `get_attachment` still returns them by id.

### get_unread
Get unread threads or unread messages. Default is summary by chat.

By default, junk and unknown-sender chats are hidden. The response carries `filtered_hidden`; pass `include_filtered=True` to include them.

```text
get_unread()                         # Summary by chat for last 7 days
get_unread(since="24h")              # Summary by chat for last 24 hours
get_unread(format="messages")        # Row-level unread messages
get_unread(include_filtered=True)    # Also show junk / unknown-sender chats
```

### send
Send a message or file attachment (requires Automation permission for Messages.app).

```text
send(to="Contact A", text="Checking in")
send(chat_id="chat123", text="Please use the latest draft")
send(chat_id="chat123", file_paths=["/path/save-the-date.jpg"])
send(to="Contact A", file_paths=["/path/reference.png"], text="Sharing the file here")
```

Rules:
- Exactly one of `to` or `chat_id`
- At least one of `text` or `file_paths`
- If both are provided, files are sent first and text is sent last

Attachment paths must be absolute (or start with `~/`). The path is
checked component by component and refused if any part of it is a symbolic link
(`/tmp`, `/var`, `/etc` are allowed and read as `/private/...`). The
file is opened without following links, must be a regular file, and is
copied from that open handle into a private `0700` directory under
`~/Pictures/imessage-max-staging/`; Messages only ever sees the copy. If
`~/Pictures` itself is a symlink on your Mac, `send` with a file will refuse
to stage until it points at a real directory.

### diagnose
Troubleshoot configuration and permission issues.

```text
diagnose()  # Returns: database status, contacts count, permissions, capabilities
```

`capabilities.verified_send` is `supported` when chat.db is readable and Messages Automation is OK, `degraded` when the database is readable but Automation is not OK, and `permission-gated` when the database is not readable.

`database.features` lists which optional `chat.db` columns exist on this Mac, keyed `table.column` (for example `message.date_edited`). When one is `false`, the tools that read it degrade instead of failing: no `reply_to` or `reply_count` without `message.thread_originator_guid`, no `edited` without `message.date_edited`, no custom-emoji reaction text without `message.associated_message_emoji`.

`contacts.status` is one of `authorized`, `limited`, `denied`, `restricted`, `not_determined`, `not_requested_headless`, `skipped_ci`, or `<status>_load_failed`. A headless process that skipped the prompt reports `not_requested_headless` and the fix names `--request-contacts-access`.

## Release checks

Before a release, work through:

- [docs/validation/2026-04-09-release-checklist.md](https://github.com/cyberpapiii/imessage-max/blob/HEAD/docs/validation/2026-04-09-release-checklist.md)
- [docs/validation/2026-03-13-send-manual-validation.md](https://github.com/cyberpapiii/imessage-max/blob/HEAD/docs/validation/2026-03-13-send-manual-validation.md)

Additional send note:
- Sends execute immediately when the destination is exact; there is no confirmation gate. Ambiguous destinations are refused with `status: "ambiguous"`. The `confirm` parameter is deprecated and ignored (kept only for compatibility). Authorization happens in the user's conversation with the agent and in the client's tool-approval UI, not server-side.

Send result semantics (text sends are verified post-send against chat.db):
- `status: "confirmed"` means the outbound row was found in chat.db within the verification window with no error; `verified_message_guid` is the evidence. It is not a delivery receipt.
- `status: "uncertain"` means transport accepted the send but the row was not found within the polling window; follow up with `get_messages`
- `status: "mismatch"` means the message landed in a different chat than intended; do not treat as success
- `status: "failed_delivery"` means the message row was found with a delivery error recorded; the message did not deliver, and `verified_message_guid` plus the error code are the evidence
- `status: "partial_failure"` means a multi-payload send dispatched some payloads before a later one failed; `message` lists what was dispatched and what failed. Retry only the failed payload, never the whole call
- `status: "sent"` means verification was unavailable (DB unreadable); transport accepted only
- `status: "pending_confirmation"` means Messages accepted an attachment send, but the file transfer was not confirmed as finished within the polling window
- `status: "failed"` means the send failed
- `status: "ambiguous"` means the target could not be resolved safely

#### Disposition and retry_safe

Every `send` response also carries `disposition` and `retry_safe`. `disposition` is about the transport (did the Apple event go out); `status` is about chat.db.

| status | disposition | retry_safe |
|---|---|---|
| confirmed, uncertain, mismatch, sent | `completed` | `false` |
| failed_delivery | `completed` | `true` |
| pending_confirmation | `may_have_completed` | `false` |
| failed (transport) | from the send | `true` only when `not_started` |
| failed (validation / resolution) | `not_started` | `true` |
| partial_failure | failing payload's disposition | `false` |
| ambiguous | `not_started` | `true` |

```json
{"status":"failed","disposition":"not_started","retry_safe":true,"error":"Could not find chat 'iMessage;-;does-not-exist' in Messages.app."}
{"status":"failed","disposition":"may_have_completed","retry_safe":false,"error":"Send operation timed out. Messages.app may be unresponsive."}
```

Notes:
- `pending_confirmation` is a normal non-fatal attachment state, not the same as a hard failure
- exact chat sends target the existing conversation identified by `chat_id`
- JSON-shaped tools return MCP `structuredContent` as well as legacy text content for older clients

Examples:
- `{"status":"confirmed","verified_message_guid":"...",...}` means delivery was verified in chat.db
- `{"status":"pending_confirmation","success":false,...}` means Messages accepted the attachment, but the MCP could not yet confirm final completion

## Troubleshooting

### Contacts showing as phone numbers

Run `diagnose` to check status. If `contacts_authorized` is false:
- Add the `imessage-max` binary to System Settings → Privacy & Security → Contacts

If `diagnose` reports `contacts.status: "not_requested_headless"`, run `imessage-max --request-contacts-access` from a terminal. Names refresh within 30 s of a Contacts change and are dropped as soon as access is revoked; no restart needed.

### "Cannot read the iMessage database" / `permission_denied`

Full Disk Access is missing for the **process that opens chat.db**, which
is the `imessage-max` binary itself for the launchd service, or the app
that spawns it for stdio clients. Run `diagnose`; `database.fix` names the
executable and the steps:

1. System Settings → Privacy & Security → Full Disk Access → add that
   executable (or its launching app).
2. If it is already listed, toggle it off and on. A grant bound to an
   earlier code signature looks present but does not work; `make
   setup-signing` gives the binary a stable identity so rebuilds keep it.
3. The grant applies only to newly launched processes. Relaunch:
   `launchctl kickstart -k gui/$(id -u)/local.imessage-max` or `cd swift && make restart`.
4. Bisect from a terminal: `sqlite3 -readonly ~/Library/Messages/chat.db 'pragma quick_check;'`.
   `ok` means your terminal has access and the server process does not;
   `unable to open database file` means the grant is missing for your user.

`make install` now ends with `make verify-db`, which calls `diagnose` over
HTTP and fails if `database.accessible` is false. Run it on its own after
changing Full Disk Access:

    cd swift && make verify-db

### "Database not found" error

`~/Library/Messages/chat.db` does not exist. Sign in to iMessage and send or
receive one message.

### Images show "attachment_offloaded" error

Some attachments are stored in iCloud, not on disk. `list_attachments` includes nested attachment summaries with `available: true/false` for each file. To download offloaded attachments, open the conversation in Messages.app.

### MCP client not loading the server

1. Check config file syntax is valid JSON
2. Verify the binary path is correct
3. Reconnect or fully restart your MCP client

## Architecture

```
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  MCP Client /   │◄───►│  iMessage Max   │◄───►│  chat.db        │
│  Agent          │     │  (Swift MCP)    │     │  (SQLite)       │
└─────────────────┘     └────────┬────────┘     └─────────────────┘
                                │
                                ▼
                        ┌─────────────────┐
                        │  Contacts.app   │
                        │  (CNContactStore)│
                        └─────────────────┘
```

## Requirements

- macOS 15+ (Sequoia or later)
- Full Disk Access permission
- Contacts permission (for name resolution)
- Automation permission for Messages.app (send only)

## Advanced setup

For HTTP mode, local background service setup, development commands, and
contributor-focused workflow details, see the Swift README. Request bodies
must arrive within 30 seconds; a stalled upload gets HTTP 408 with a JSON-RPC
error body, and connections idle for 60 seconds are closed. See also:

- [swift/README.md](https://github.com/cyberpapiii/imessage-max/blob/HEAD/swift/README.md)

## License

MIT

