Skip to main content
Glama
cyberpapiii

iMessage Max

by cyberpapiii

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/.

Related MCP server: jons-mcp-imessage

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

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

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

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

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

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

brew tap cyberpapiii/tap
brew install imessage-max

From source

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:

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

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

{
  "mcpServers": {
    "imessage": {
      "command": "/opt/homebrew/Cellar/imessage-max/VERSION/bin/imessage-max"
    }
  }
}

For source builds:

{
  "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.

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.

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.

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.

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:

{
  "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.

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.

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

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.

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.

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

get_active_conversations

Find chats with recent back-and-forth activity.

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.

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.

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).

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.

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:

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

{"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:

License

MIT

Maintenance

ActivityMaintained
ResponsivenessUnresponsive

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables Claude to send and read iMessages on macOS, with smart contact lookup, message history retrieval, and cross-conversation search using natural language commands.
    5
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Enables AI assistants to read iMessage history and send messages on macOS. Supports conversation listing, message search with keyword and semantic modes, contact lookup, and sending messages to existing conversations.
    13
    11
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants to read, search, and send iMessages, manage contacts, and access attachments on macOS.
    16
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to read the entire Apple Messages (iMessage/SMS) history on a Mac through a read-only, batched tool that supports listing chats, retrieving transcripts, polling recent messages, and searching message bodies via REST or streamable HTTP MCP.
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyberpapiii/imessage-max'

If you have feedback or need assistance with the MCP directory API, please join our Discord server