Skip to main content
Glama
metaember

bluebubbles-mcp

by metaember

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

Related MCP server: mac-messages-mcp

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

Install Server
A
license - permissive license
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

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

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/metaember/bluebubbles-mcp'

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