{
  "markdown": "<p align=\"center\">\n  <img src=\"icon.png\" alt=\"iMessage Max\" width=\"128\">\n</p>\n\n# iMessage Max\n\nAn 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.\n\nWritten in Swift. One binary, no runtime dependencies.\n\n## Distribution status\n\nThe project now ships a single Swift implementation:\n- GitHub releases\n- Homebrew\n- source builds\n- Codex plugin metadata\n- Claude Desktop MCPB metadata\n\nThe old Python package has been retired and removed from the repository.\nEverything current lives under `swift/`.\n\n## Features\n\n- 13 tools shaped around questions people actually ask, not around database tables\n- Phone numbers resolve to names through macOS Contacts\n- Three image variants: vision (1568px), thumb (400px), full (original), so a photo does not have to arrive at full size\n- Messages group into sessions, split on gaps of 4 hours or more\n- Attachment listings say whether each file is on disk or offloaded to iCloud\n- Raw SQLite3 and Core Image GPU acceleration\n- Reads chat.db only. Sending needs Automation permission for Messages.app\n\n## Why this exists\n\nMost iMessage tools expose raw database structures, requiring 3-5 tool calls per user intent. This MCP provides intent-aligned tools:\n\n```\n\"What did Contact A and I talk about yesterday?\"\n→ find_chat(participants=[\"Contact A\"]) + get_messages(since=\"yesterday\")\n\n\"Show me the exact details for this thread before I reply\"\n→ get_chat_details(chat_id=\"chat123\")\n\n\"Show me photos from the group chat\"\n→ list_attachments(chat_id=\"chat123\", type=\"image\")\n\n\"Find where we discussed the launch timeline\"\n→ search(query=\"launch timeline\")\n```\n\n## Common agent workflows\n\nThe tools work best when an agent uses them as short workflows instead of isolated one-off calls.\n\nAgents 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.\"\n\n### Find the right conversation, then read it\n\n```text\nfind_chat(participants=[\"Contact A\"])\nget_chat_details(chat_id=\"chat123\")\nget_messages(chat_id=\"chat123\", since=\"yesterday\", limit=50)\n```\n\nUse this when the person matters more than the exact thread id.\n\n### Search first, then zoom in\n\n```text\nsearch(query=\"launch timeline\", limit=10)\nget_context(message_id=\"msg_456\", before=5, after=10)\n```\n\nUse this when you know the topic but not where it was discussed.\n\n### Check what needs attention\n\n```text\nget_unread()\nget_active_conversations(hours=24, min_exchanges=2)\n```\n\nUse this to surface unread threads and active conversations after a broad chat-list sweep.\n\n### Work with attachments safely\n\n```text\nlist_attachments(chat_id=\"chat123\", type=\"image\", since=\"30d\")\nget_attachment(attachment_id=\"att123\", variant=\"vision\")\n```\n\nUse `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.\n\n### Send with exact targeting when it matters\n\n```text\nfind_chat(participants=[\"Contact A\", \"Contact B\"])\nsend(chat_id=\"chat456\", text=\"Please use the latest draft\")\n```\n\nFor sensitive sends, prefer resolving the exact chat first and then using `chat_id` so the message lands in the intended thread.\n\n## Installation\n\n### Homebrew (recommended)\n\n```bash\nbrew tap cyberpapiii/tap\nbrew install imessage-max\n```\n\n### From source\n\n```bash\ngit clone https://github.com/cyberpapiii/imessage-max.git\ncd imessage-max/swift\nswift build -c release\n\n# Binary is at .build/release/imessage-max\n```\n\nFor local development, advanced setup, and the signed install workflow, see:\n\n- [swift/README.md](swift/README.md)\n\n## Protocol support\n\niMessage Max is a dual-era MCP server. Both transports (stdio and HTTP)\nserve both eras concurrently, selected per request:\n\n| Era | Revisions | Lifecycle | Selected by |\n| --- | --- | --- | --- |\n| Modern | `2026-07-28` | Stateless, per-request `_meta` | `io.modelcontextprotocol/protocolVersion` in the request `_meta` (or `server/discover`) |\n| Legacy | `2025-03-26` … `2025-11-25` | `initialize` + session | `initialize` request / `Mcp-Session-Id` |\n\nModern clients probe with `server/discover` and send the required\n`MCP-Protocol-Version`, `Mcp-Method`, and (for `tools/call`) `Mcp-Name`\nheaders over HTTP. Legacy clients keep working unchanged. No client\nmigration is required, and legacy support stays as long as real clients\ndepend on it.\n\nThe server implements tools only. It has no prompts, resources,\ncompletion, subscriptions, tasks, or MRTR flows, on purpose. The official\nconformance suite runs against both eras with the documented baseline in\n`docs/conformance-baseline.yml`.\n\n## Client icon metadata\n\niMessage Max ships icons for the main MCP protocol surface and the client\npackaging surfaces that use their own metadata:\n\n- MCP `2025-11-25` initialize responses include PNG `serverInfo.icons`.\n- Each tool advertises a compact PNG tool icon.\n- Codex plugin metadata lives in `.codex-plugin/plugin.json` and uses\n  `assets/codex/icon.png` plus `assets/codex/logo.png`.\n- Claude Desktop / MCPB metadata lives in `mcpb/manifest.json` and uses PNG\n  assets under `mcpb/assets/`.\n\nThe committed PNG source set is under `assets/icons/` at `16x16`, `32x32`,\n`64x64`, `128x128`, `256x256`, and `512x512`.\n\n## Setup\n\n### 1. Grant Full Disk Access\n\nRequired to read `~/Library/Messages/chat.db`:\n\n1. Open System Settings → Privacy & Security → Full Disk Access\n2. Click + to add the binary\n\nFor 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:\n```bash\n# Open the folder containing the actual binary\nopen $(dirname $(readlink -f $(which imessage-max)))\n```\n\nFor source builds, add `.build/release/imessage-max` from your clone directory.\nAfter changing the grant, relaunch the server; macOS applies Full Disk Access only to processes started after the change.\n\n> In the file picker, press ⌘+Shift+G and paste the path to go straight there.\n\n### 2. Grant Contacts access\n\nRequired 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.\n\nSystem Settings → Privacy & Security → Contacts → add `imessage-max` is the manual alternative.\n\n### 3. Configure your MCP client\n\nAdd `imessage-max` to your MCP client's server configuration.\n\nMany MCP clients use a JSON structure like this:\n\nFor Homebrew:\n```json\n{\n  \"mcpServers\": {\n    \"imessage\": {\n      \"command\": \"/opt/homebrew/Cellar/imessage-max/VERSION/bin/imessage-max\"\n    }\n  }\n}\n```\n\nFor source builds:\n```json\n{\n  \"mcpServers\": {\n    \"imessage\": {\n      \"command\": \"/path/to/imessage-max/swift/.build/release/imessage-max\"\n    }\n  }\n}\n```\n\nIf your client uses a different config format, point it at the same binary path.\n\n### 4. Reconnect your MCP client\n\nAfter 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`.\n\n## Tools\n\n### find_chat\nFind chats by participants, name, or recent content.\n\nBy 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.\n\n```text\nfind_chat(participants=[\"Contact A\"])              # Find a direct chat\nfind_chat(participants=[\"Contact A\", \"Contact B\"]) # Find a group with both\nfind_chat(name=\"Project Group\")                    # Find by chat name\nfind_chat(contains_recent=\"latest draft\")          # Find by recent content\nfind_chat(name=\"Project Group\", include_filtered=True)  # Also search junk / unknown-sender chats\n```\n\n### get_chat_details\nInspect a known thread without opening the full conversation.\n\n```text\nget_chat_details(chat_id=\"chat123\")                          # Participants, handles, state, last message\nget_chat_details(chat_id=\"chat123\", include_shared_summary=false) # Skip recent shared summary\n```\n\n### get_messages\nRetrieve messages with flexible filtering. Returns metadata for media. Explicit `chat_id` lookups are never filtered.\n\n```text\nget_messages(chat_id=\"chat123\", limit=50)           # Recent messages\nget_messages(chat_id=\"chat123\", since=\"24h\")        # Last 24 hours\nget_messages(chat_id=\"chat123\", from_person=\"Contact A\")  # From specific person\nget_messages(chat_id=\"chat123\", has=\"links\")        # Messages that contain a URL\n```\n\n`has` filters by content type (`links`, `attachments`, `images`). `links` includes link messages Messages stores as URL preview balloons (the common case on macOS 26).\n\nGroup system messages (renames, members added or removed, someone\nleaving) come back with `text: null` and an `event` object:\n`{\"type\": \"rename\", \"title\": \"Trip\"}`, `{\"type\": \"participant_added\", \"participant\": \"Alice\"}`,\n`{\"type\": \"participant_removed\", ...}`, `{\"type\": \"left\"}`, or\n`{\"type\": \"other\", \"item_type\": N}` for event kinds the server does not\nname. Chat previews describe the same events in words\n(\"renamed the group to Trip\").\n\nReactions, replies, edits: each message may include `reactions`\n(`[\"❤️ alice\"]` for standard tapbacks; custom emoji show the emoji\nitself; sticker reactions use the token `🩵 sticker`), `reply_to`\n(`msg_<rowid>` of the originator), `reply_count`, and `edited: true`.\nRemoved tapbacks are omitted. All four fields are optional and omitted\nwhen empty. `search` and `get_context` carry the same four fields.\nThese are read-only; `diagnose` still reports `tapbacks` and\n`edit_unsend` as `unsupported` because the server cannot send them.\n\n### get_messages_since\nNew 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`.\n\n```text\nget_messages_since()                          # Current cursor only\nget_messages_since(since_rowid=234000)        # Messages after that row\nget_messages_since(since_rowid=234000, limit=50)\nget_messages_since(since_rowid=234000, chat_id=\"chat123\")\nget_messages_since(since_rowid=234000, include_filtered=True)\n```\n\n| Parameter | Type | Default | Meaning |\n|-----------|------|---------|---------|\n| `since_rowid` | integer | omitted | Exclusive ROWID cursor. Omit or pass `-1` for the current cursor only. |\n| `chat_id` | string | omitted | Restrict to one chat (`chat123` or `123`) |\n| `limit` | integer | 100 | Maximum messages to return (1–500) |\n| `include_filtered` | boolean | false | Include junk / unknown-sender chats |\n| `include_reactions` | boolean | true | Attach reaction strings to returned messages |\n\nExample response:\n\n```json\n{\n  \"since_rowid\": 234000,\n  \"messages\": [\n    {\n      \"id\": \"msg_234001\",\n      \"rowid\": 234001,\n      \"chat\": {\"id\": \"chat12\", \"name\": \"Alice Smith\"},\n      \"from\": \"Alice Smith\",\n      \"text\": \"on my way\",\n      \"ts\": \"2026-09-02T15:00:00Z\"\n    },\n    {\n      \"id\": \"msg_234010\",\n      \"rowid\": 234010,\n      \"chat\": {\"id\": \"chat40\", \"name\": \"Weekend\"},\n      \"from\": \"me\",\n      \"text\": \"see you there\",\n      \"ts\": \"2026-09-02T15:01:00Z\"\n    }\n  ],\n  \"next_rowid\": 234050,\n  \"has_more\": false,\n  \"current_rowid\": 234050,\n  \"stalled\": false,\n  \"filtered_hidden\": 1\n}\n```\n\nPolling 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.\n\n### get_attachment\nRetrieve image content by attachment ID with resolution variants.\n\n```text\nget_attachment(attachment_id=\"att123\")                 # Default: vision (1568px)\nget_attachment(attachment_id=\"att123\", variant=\"thumb\") # Quick preview (400px)\nget_attachment(attachment_id=\"att123\", variant=\"full\")  # Original resolution\n```\n\n| Variant | Resolution | Use Case | Token Cost |\n|---------|------------|----------|------------|\n| `vision` (default) | 1568px | AI analysis, OCR | ~1,600 tokens |\n| `thumb` | 400px | Quick preview | ~200 tokens |\n| `full` | Original | Maximum detail | Varies |\n\n### list_chats\nBrowse recent chats with previews.\n\nBy 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.\n\n```text\nlist_chats(limit=20)          # Recent chats\nlist_chats(is_group=True)     # Only group chats\nlist_chats(since=\"7d\")        # Active in last week\nlist_chats(include_filtered=True)   # Also show junk / unknown-sender chats\n```\n\n### search\nFull-text search across messages.\n\nBy default, junk and unknown-sender chats are hidden. The response carries `filtered_hidden`; pass `include_filtered=True` to search them too.\n\n```text\nsearch(query=\"draft\")                           # Search all messages\nsearch(query=\"budget\", from_person=\"Contact A\") # From specific person\nsearch(query=\"launch\", is_group=True)           # Only in group chats\nsearch(query=\"draft\", include_filtered=True)    # Also search junk / unknown-sender chats\n```\n\n### get_context\nGet messages surrounding a specific message.\n\n```text\nget_context(message_id=\"msg_123\", before=5, after=10)\n```\n\n### get_active_conversations\nFind chats with recent back-and-forth activity.\n\n```text\nget_active_conversations(hours=24)\nget_active_conversations(is_group=True, min_exchanges=3)\n```\n\n### list_attachments\nBrowse shared items grouped by message. Each row includes exact attachment ids for follow-up fetches.\n\n```text\nlist_attachments(type=\"image\", since=\"7d\")\nlist_attachments(chat_id=\"chat123\", type=\"any\")\n```\n\nAttachments Messages hides (`hide_attachment`, e.g. link-preview payloads) are not listed; `get_attachment` still returns them by id.\n\n### get_unread\nGet unread threads or unread messages. Default is summary by chat.\n\nBy default, junk and unknown-sender chats are hidden. The response carries `filtered_hidden`; pass `include_filtered=True` to include them.\n\n```text\nget_unread()                         # Summary by chat for last 7 days\nget_unread(since=\"24h\")              # Summary by chat for last 24 hours\nget_unread(format=\"messages\")        # Row-level unread messages\nget_unread(include_filtered=True)    # Also show junk / unknown-sender chats\n```\n\n### send\nSend a message or file attachment (requires Automation permission for Messages.app).\n\n```text\nsend(to=\"Contact A\", text=\"Checking in\")\nsend(chat_id=\"chat123\", text=\"Please use the latest draft\")\nsend(chat_id=\"chat123\", file_paths=[\"/path/save-the-date.jpg\"])\nsend(to=\"Contact A\", file_paths=[\"/path/reference.png\"], text=\"Sharing the file here\")\n```\n\nRules:\n- Exactly one of `to` or `chat_id`\n- At least one of `text` or `file_paths`\n- If both are provided, files are sent first and text is sent last\n\nAttachment paths must be absolute (or start with `~/`). The path is\nchecked component by component and refused if any part of it is a symbolic link\n(`/tmp`, `/var`, `/etc` are allowed and read as `/private/...`). The\nfile is opened without following links, must be a regular file, and is\ncopied from that open handle into a private `0700` directory under\n`~/Pictures/imessage-max-staging/`; Messages only ever sees the copy. If\n`~/Pictures` itself is a symlink on your Mac, `send` with a file will refuse\nto stage until it points at a real directory.\n\n### diagnose\nTroubleshoot configuration and permission issues.\n\n```text\ndiagnose()  # Returns: database status, contacts count, permissions, capabilities\n```\n\n`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.\n\n`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`.\n\n`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`.\n\n## Release checks\n\nBefore a release, work through:\n\n- [docs/validation/2026-04-09-release-checklist.md](docs/validation/2026-04-09-release-checklist.md)\n- [docs/validation/2026-03-13-send-manual-validation.md](docs/validation/2026-03-13-send-manual-validation.md)\n\nAdditional send note:\n- 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.\n\nSend result semantics (text sends are verified post-send against chat.db):\n- `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.\n- `status: \"uncertain\"` means transport accepted the send but the row was not found within the polling window; follow up with `get_messages`\n- `status: \"mismatch\"` means the message landed in a different chat than intended; do not treat as success\n- `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\n- `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\n- `status: \"sent\"` means verification was unavailable (DB unreadable); transport accepted only\n- `status: \"pending_confirmation\"` means Messages accepted an attachment send, but the file transfer was not confirmed as finished within the polling window\n- `status: \"failed\"` means the send failed\n- `status: \"ambiguous\"` means the target could not be resolved safely\n\n#### Disposition and retry_safe\n\nEvery `send` response also carries `disposition` and `retry_safe`. `disposition` is about the transport (did the Apple event go out); `status` is about chat.db.\n\n| status | disposition | retry_safe |\n|---|---|---|\n| confirmed, uncertain, mismatch, sent | `completed` | `false` |\n| failed_delivery | `completed` | `true` |\n| pending_confirmation | `may_have_completed` | `false` |\n| failed (transport) | from the send | `true` only when `not_started` |\n| failed (validation / resolution) | `not_started` | `true` |\n| partial_failure | failing payload's disposition | `false` |\n| ambiguous | `not_started` | `true` |\n\n```json\n{\"status\":\"failed\",\"disposition\":\"not_started\",\"retry_safe\":true,\"error\":\"Could not find chat 'iMessage;-;does-not-exist' in Messages.app.\"}\n{\"status\":\"failed\",\"disposition\":\"may_have_completed\",\"retry_safe\":false,\"error\":\"Send operation timed out. Messages.app may be unresponsive.\"}\n```\n\nNotes:\n- `pending_confirmation` is a normal non-fatal attachment state, not the same as a hard failure\n- exact chat sends target the existing conversation identified by `chat_id`\n- JSON-shaped tools return MCP `structuredContent` as well as legacy text content for older clients\n\nExamples:\n- `{\"status\":\"confirmed\",\"verified_message_guid\":\"...\",...}` means delivery was verified in chat.db\n- `{\"status\":\"pending_confirmation\",\"success\":false,...}` means Messages accepted the attachment, but the MCP could not yet confirm final completion\n\n## Troubleshooting\n\n### Contacts showing as phone numbers\n\nRun `diagnose` to check status. If `contacts_authorized` is false:\n- Add the `imessage-max` binary to System Settings → Privacy & Security → Contacts\n\nIf `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.\n\n### \"Cannot read the iMessage database\" / `permission_denied`\n\nFull Disk Access is missing for the **process that opens chat.db**, which\nis the `imessage-max` binary itself for the launchd service, or the app\nthat spawns it for stdio clients. Run `diagnose`; `database.fix` names the\nexecutable and the steps:\n\n1. System Settings → Privacy & Security → Full Disk Access → add that\n   executable (or its launching app).\n2. If it is already listed, toggle it off and on. A grant bound to an\n   earlier code signature looks present but does not work; `make\n   setup-signing` gives the binary a stable identity so rebuilds keep it.\n3. The grant applies only to newly launched processes. Relaunch:\n   `launchctl kickstart -k gui/$(id -u)/local.imessage-max` or `cd swift && make restart`.\n4. Bisect from a terminal: `sqlite3 -readonly ~/Library/Messages/chat.db 'pragma quick_check;'`.\n   `ok` means your terminal has access and the server process does not;\n   `unable to open database file` means the grant is missing for your user.\n\n`make install` now ends with `make verify-db`, which calls `diagnose` over\nHTTP and fails if `database.accessible` is false. Run it on its own after\nchanging Full Disk Access:\n\n    cd swift && make verify-db\n\n### \"Database not found\" error\n\n`~/Library/Messages/chat.db` does not exist. Sign in to iMessage and send or\nreceive one message.\n\n### Images show \"attachment_offloaded\" error\n\nSome 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.\n\n### MCP client not loading the server\n\n1. Check config file syntax is valid JSON\n2. Verify the binary path is correct\n3. Reconnect or fully restart your MCP client\n\n## Architecture\n\n```\n┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐\n│  MCP Client /   │◄───►│  iMessage Max   │◄───►│  chat.db        │\n│  Agent          │     │  (Swift MCP)    │     │  (SQLite)       │\n└─────────────────┘     └────────┬────────┘     └─────────────────┘\n                                │\n                                ▼\n                        ┌─────────────────┐\n                        │  Contacts.app   │\n                        │  (CNContactStore)│\n                        └─────────────────┘\n```\n\n## Requirements\n\n- macOS 15+ (Sequoia or later)\n- Full Disk Access permission\n- Contacts permission (for name resolution)\n- Automation permission for Messages.app (send only)\n\n## Advanced setup\n\nFor HTTP mode, local background service setup, development commands, and\ncontributor-focused workflow details, see the Swift README. Request bodies\nmust arrive within 30 seconds; a stalled upload gets HTTP 408 with a JSON-RPC\nerror body, and connections idle for 60 seconds are closed. See also:\n\n- [swift/README.md](swift/README.md)\n\n## License\n\nMIT\n",
  "bytes": 23775,
  "sha": "78455abb44b2e7f4f00bc56ee78664178bab343dd33f57bca2e1a47f5ff4f7c2",
  "repo_slug": "cyberpapiii/imessage-max",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_cyberpapiii_imessage_max_000eb7d7/readme"
}