Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bluebubbles-mcp

MCP server for BlueBubbles — access iMessage from any MCP client.

Built from scratch with no third-party MCP dependencies beyond the official mcp SDK and httpx.

Prerequisites

Setup

git clone https://github.com/metaember/bluebubbles-mcp.git
cd bluebubbles-mcp
uv sync

Configuration

Add to your MCP client config (e.g. Claude Code ~/.claude/settings.json):

{
  "mcpServers": {
    "bluebubbles": {
      "command": "uv",
      "args": ["--directory", "/path/to/bluebubbles-mcp", "run", "python", "-m", "bb_mcp.server"],
      "env": {
        "BLUEBUBBLES_URL": "https://your-bluebubbles-server",
        "BLUEBUBBLES_PASSWORD": "your-server-password"
      }
    }
  }
}

Restricting write recipients (allowlist)

By default the server can message anyone. Set BLUEBUBBLES_WRITE_ALLOWLIST to a comma-separated list of phone numbers and/or emails to restrict every write (send, reaction, attachment, typing, scheduling, group edits, delete/leave) to those recipients. It is enforced server-side, so an MCP client — including one reaching the HTTP transport — cannot bypass it.

"BLUEBUBBLES_WRITE_ALLOWLIST": "+15551234567, partner@example.com",
"BLUEBUBBLES_ALLOWLIST_REGION": "US"  // optional, default region for parsing local numbers

Behavior:

  • Unset → unrestricted (backward compatible). Set but empty ("") → deny all, a safe failure mode.
  • Numbers are matched in normalized E.164 form, so (555) 123-4567 and +15551234567 are equivalent; emails match case-insensitively.
  • For a group chat, every participant must be on the list or the write is blocked. A chat whose participants can't be resolved is denied (fail-closed).
  • Read tools are not restricted, and the message-GUID writes (edit_message, unsend_message) aren't covered — they only act on an already-sent message and can't reach a new recipient. A BLUEBUBBLES_READ_ALLOWLIST may be added later.

Private API vs AppleScript

BlueBubbles can drive Messages two ways. AppleScript works on any install with no extra setup but only sends plain texts/attachments. The Private API (requires the BlueBubbles Helper + partially disabled SIP) additionally unlocks tapbacks, edit/unsend, typing indicators, read receipts, group management, threaded replies, and SMS sending.

On startup the server reads /server/info and adapts itself to what's actually available:

  • Sends use the Private API when present, and fall back to AppleScript when not — so send_message works on a bare install instead of erroring.
  • Tools that require the Private API (reactions, edit/unsend, typing, read receipts, group management, iMessage/FaceTime availability checks) are hidden from the tool list when it's unavailable, so a client never sees a tool that could only fail.

Override the auto-detection with BLUEBUBBLES_PRIVATE_API:

"BLUEBUBBLES_PRIVATE_API": "auto"   // default: introspect /server/info
// "true"  — force-enable (assume the Private API is set up)
// "false" — force-disable (AppleScript only; hide Private API tools)

If /server/info can't be read at startup, detection fails open (assumes the Private API is available) so a transient blip doesn't hide half the toolset.

The same /server/info read also discovers the user's own iMessage address, surfaced by the get_my_address tool. Override it if detection is wrong or you run multiple handles:

"BLUEBUBBLES_MY_ADDRESS": "+15551234567"  // or you@icloud.com

Contact names

Message and chat data from BlueBubbles carries raw phone numbers and emails, not names. To save the model from constantly cross-referencing numbers, read responses are enriched with a contactName beside each handle, and find_contact / find_chats let it reach people by name instead of number.

Resolution is lazy and cached per session: a response's addresses are looked up in a single batched /contact/query the first time they're seen, then reused. Disable it (for leaner responses or if your contact DB is slow) with:

"BLUEBUBBLES_RESOLVE_NAMES": "false"  // default: enabled

find_contact / find_chats still work when this is off — they're explicit.

Freshness guard

An agent can reply to a conversation against a stale snapshot — it read the thread a while ago, a new message arrived in the gap, and it answers without seeing it. The freshness guard enforces, server-side and per agent, that a send into a chat only goes through if that agent read the chat within the last hour and nothing new has arrived since. (A consequence: you must read an existing chat before sending into it — good hygiene for replies. Starting a brand-new conversation via create_chat is unaffected; it's for first contact only and, with the guard on, refuses to reach an existing chat — use send_message for those.)

How it works. The guard keeps a per-agent watermark — the newest message each agent has seen in each chat (optimistic concurrency, like an ETag on the thread):

  • Reading a chat with get_chat_messages records the watermark. Only that tool records — a scan like get_unread_chats doesn't — so to reply you must deliberately open the actual thread.
  • Before a send, the server re-checks the chat's live newest message. If anything (from anyone — the other person, you on another device, or another agent) arrived since your watermark, or your read is older than the TTL, the send is blocked with a note to re-read and re-plan.
  • Your own send advances your watermark, so sending doesn't block your next send.

See docs/freshness-guard.md for the full design.

On by default. Disable with BLUEBUBBLES_FRESHNESS=off. Tuning:

"BLUEBUBBLES_FRESHNESS": "off",                  // default: on
"BLUEBUBBLES_FRESHNESS_IDENTITY": "session",     // how agents are told apart (default)
"BLUEBUBBLES_WATERMARK_TTL_SECONDS": "3600",     // how long a read stays "fresh" (default 1h)
"BLUEBUBBLES_WATERMARK_MAX_AGENTS": "10000"      // memory backstop (default 10000)

The freshness check is per agent, so it needs to tell agents apart. BLUEBUBBLES_FRESHNESS_IDENTITY selects how:

  • session (default): use the MCP transport session — automatic and zero-config for stdio (one agent) and direct HTTP (one session per connecting client). Over HTTP this relies on stateful sessions (FastMCP's default); the server refuses to start if you combine session-identity freshness with stateless HTTP, since every send would be blocked.
  • meta: identify agents by a per-agent agentId the caller stamps into request _meta. Required only behind a pooling proxy/airlock, where every agent shares one transport session so session can't distinguish them. See docs/freshness-guard.md for the airlock contract.

Compact responses & sender filtering

Message reads return a compact projection by default — the fields an assistant needs (guid, text, handle + contactName, isFromMe, timestamps, attachment names, reaction/reply linkage) instead of the full raw BlueBubbles objects, which cuts token usage substantially. Pass extended=true on any read tool to get the complete raw fields.

Message reads (get_chat_messages, search_messages, get_recent_messages) also accept from_address to filter by who sent each message — pass a phone number / email, or "me" for the user's own messages. Filtering is by true sender (the user when isFromMe, otherwise the message's handle), so it's correct in both 1:1 and group chats.

Tools

Tool Description Annotations
ping Check server connectivity read-only
get_server_info Server info and health read-only
get_my_address The user's own iMessage address (to identify their own messages) read-only
list_chats List conversations by recent activity read-only
get_chat Chat details with participants read-only
get_chat_messages Messages from a chat read-only
search_messages Search by text, chat, time range read-only
get_message Single message by GUID read-only
get_contacts All contacts read-only
lookup_contact Look up name by phone/email read-only
find_contact Find contacts by name (phone/email unknown) read-only
find_chats Find chats involving a contact by name read-only
check_imessage Check iMessage registration read-only
check_facetime Check FaceTime registration read-only
query_handles List/search known handles read-only
get_handle Get a handle by address read-only
get_focus_status Contact's Focus / Do Not Disturb status read-only
find_my_devices Find My — your devices' locations read-only
find_my_friends Find My — friends' locations read-only
list_scheduled_messages List future messages read-only
get_scheduled_message Get one scheduled message by ID read-only
get_recent_messages Messages from last N minutes across all chats read-only
get_unread_chats Chats with unread messages + their latest messages read-only
get_attachment_info Attachment metadata read-only
download_attachment Download attachment as base64 read-only
get_group_icon Download a group chat's icon read-only
mark_chat_read Send read receipt idempotent, open-world
mark_chat_unread Mark chat unread (local) idempotent
rename_group Rename a group chat idempotent
set_group_icon Set a group chat's icon idempotent
start_typing Show typing indicator open-world
stop_typing Stop typing indicator open-world
send_message Send to an existing chat (1:1 or group) open-world
create_chat Start a new 1:1 conversation by phone/email open-world
create_group_chat Create a group chat + first message open-world
send_attachment Send a file attachment open-world
send_multipart Send text + attachments as one message open-world
send_reaction Tapback reaction open-world
edit_message Edit a sent message open-world
schedule_message Schedule a future message open-world
update_scheduled_message Update a scheduled message open-world
add_participant Add to group chat open-world
unsend_message Retract a message destructive, open-world
remove_participant Remove from group chat destructive, open-world
leave_chat Leave a group chat destructive, open-world
remove_group_icon Remove a group chat's icon destructive, open-world
delete_message Delete a single message destructive, open-world
delete_chat Delete a conversation destructive, open-world
delete_scheduled_message Cancel scheduled message destructive, open-world

Run over HTTP (Docker)

The server speaks stdio by default. Set MCP_TRANSPORT=streamable-http to serve over Streamable HTTP instead, so any HTTP-capable MCP client can connect at http://<host>:8000/mcp. The Docker image sets this for you.

docker build -t bluebubbles-mcp .
docker run --rm -e BLUEBUBBLES_URL -e BLUEBUBBLES_PASSWORD -p 8000:8000 bluebubbles-mcp

As a Compose service — credentials live in this container's own environment (an env_file or Docker secrets), so they stay isolated to this tool:

services:
  bluebubbles-mcp:
    build: .
    environment:
      BLUEBUBBLES_URL: ${BLUEBUBBLES_URL}
      BLUEBUBBLES_PASSWORD: ${BLUEBUBBLES_PASSWORD}
    restart: unless-stopped

Notes:

  • The HTTP endpoint is unauthenticated, so don't expose it publicly — keep it on a private/internal network (and drop the published ports: if a co-located client reaches it over the Compose network).
  • BLUEBUBBLES_URL must be reachable from inside the container — use a hostname/IP the container can resolve (e.g. the BlueBubbles host's LAN address or host.docker.internal), not localhost.

License

MIT

About

MCP server for BlueBubbles iMessage bridge

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages