iMessage Max
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@iMessage MaxWhat did Nick say about the dinner plans yesterday?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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
Homebrew (recommended)
brew tap cyberpapiii/tap
brew install imessage-maxFrom source
git clone https://github.com/cyberpapiii/imessage-max.git
cd imessage-max/swift
swift build -c release
# Binary is at .build/release/imessage-maxFor 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 |
| Stateless, per-request |
|
Legacy |
|
|
|
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-25initialize responses include PNGserverInfo.icons.Each tool advertises a compact PNG tool icon.
Codex plugin metadata lives in
.codex-plugin/plugin.jsonand usesassets/codex/icon.pngplusassets/codex/logo.png.Claude Desktop / MCPB metadata lives in
mcpb/manifest.jsonand uses PNG assets undermcpb/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:
Open System Settings → Privacy & Security → Full Disk Access
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 chatsget_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 summaryget_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 URLhas 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 |
| integer | omitted | Exclusive ROWID cursor. Omit or pass |
| string | omitted | Restrict to one chat ( |
| integer | 100 | Maximum messages to return (1–500) |
| boolean | false | Include junk / unknown-sender chats |
| 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 resolutionVariant | Resolution | Use Case | Token Cost |
| 1568px | AI analysis, OCR | ~1,600 tokens |
| 400px | Quick preview | ~200 tokens |
| 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 chatssearch
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 chatsget_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 chatssend
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
toorchat_idAt least one of
textorfile_pathsIf 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, capabilitiescapabilities.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". Theconfirmparameter 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_guidis 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 withget_messagesstatus: "mismatch"means the message landed in a different chat than intended; do not treat as successstatus: "failed_delivery"means the message row was found with a delivery error recorded; the message did not deliver, andverified_message_guidplus the error code are the evidencestatus: "partial_failure"means a multi-payload send dispatched some payloads before a later one failed;messagelists what was dispatched and what failed. Retry only the failed payload, never the whole callstatus: "sent"means verification was unavailable (DB unreadable); transport accepted onlystatus: "pending_confirmation"means Messages accepted an attachment send, but the file transfer was not confirmed as finished within the polling windowstatus: "failed"means the send failedstatus: "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 |
|
|
failed_delivery |
|
|
pending_confirmation |
|
|
failed (transport) | from the send |
|
failed (validation / resolution) |
|
|
partial_failure | failing payload's disposition |
|
ambiguous |
|
|
{"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_confirmationis a normal non-fatal attachment state, not the same as a hard failureexact chat sends target the existing conversation identified by
chat_idJSON-shaped tools return MCP
structuredContentas 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-maxbinary 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:
System Settings → Privacy & Security → Full Disk Access → add that executable (or its launching app).
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-signinggives the binary a stable identity so rebuilds keep it.The grant applies only to newly launched processes. Relaunch:
launchctl kickstart -k gui/$(id -u)/local.imessage-maxorcd swift && make restart.Bisect from a terminal:
sqlite3 -readonly ~/Library/Messages/chat.db 'pragma quick_check;'.okmeans your terminal has access and the server process does not;unable to open database filemeans 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
Check config file syntax is valid JSON
Verify the binary path is correct
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
This server cannot be installed
Maintenance
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
Let ChatGPT, Claude & Cursor use your Mac: email, calendar, iMessage, Teams, files. Local, free.
Carbon Voice MCP serves as a bridge that connects AI assistants like ChatGPT, Claude, and Cursor to a user's Carbon Voice account, turning voice messages and conversations into a private, on-demand knowledge base. It provides 28 specialized tools for comprehensive voice messaging management, including creating and sending messages, accessing conversation history with instant transcription, running AI actions (summarization, TLDR generation, meeting notes), and managing workspace collaboration through folders, contacts, and team communications.
MCP connector for iMessage & Contacts via a local Mac agent + Vercel relay
Communication stack for AI agents: SMS, AI voice calls, phone numbers, and account events.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceEnables Claude to send and read iMessages on macOS, with smart contact lookup, message history retrieval, and cross-conversation search using natural language commands.5MIT
- AlicenseAqualityDmaintenanceEnables 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.1311MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants to read, search, and send iMessages, manage contacts, and access attachments on macOS.16MIT
- AlicenseNot gradedqualityCmaintenanceEnables 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
Appeared in Searches
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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