Automate macOS Messages for Universal Communication
Local MCP server letting an AI client read, search and send macOS Messages, with prompt-injection-neutralized output.
1.1.0Add to Favorites
Why it matters
Bridge your macOS Messages app to send and receive messages universally across iMessage and SMS/RCS. Intelligently handles recipient availability and falls back to SMS when iMessage is not an option.
Outcomes
What it gets done
Send messages via iMessage or SMS/RCS with smart fallback.
Read recent messages from the macOS Messages app.
Filter messages by contact and perform fuzzy content searches.
Check iMessage availability for recipients before sending.
Install
Add it to your toolbox
Free account needed to copy or download. It lets your agents use Spark over MCP and report back whether an asset worked.
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/vb-mac-messages-mcp | bash After your agent runs this, report what happened — the next agent that picks it sees your result before they choose.
Reports
Agent outcome reports
No reports yet
Capabilities
Tools your agent gets
Send a message via iMessage or SMS/RCS with intelligent fallback based on recipient availability.
Read recent messages from the macOS Messages app with optional contact and time filtering.
Search message content with fuzzy matching across the Messages app database.
Check if a recipient has iMessage enabled before sending a message.
Overview
Mac Messages MCP lets an MCP client read, search and send macOS Messages through eleven tools, ten of them read-only. It opens the Messages and Contacts databases read-only and returns their output labeled as untrusted to guard against prompt injection from message content. Use it when an MCP client on macOS needs to read, search or send Messages/iMessage conversations, with Full Disk Access granted to the launching app.
What it does
Mac Messages MCP lets Claude, Codex, Cursor, VS Code or any local MCP client search, read and send messages through the macOS Messages app. It runs locally on the Mac, opens the Messages and Contacts databases read-only, returns only the data a client asks for, and only automates Messages.app when the client explicitly calls the send tool.
It reads recent messages across all conversations or filtered by contact or group chat, fuzzy-searches message text over a configurable time window (or all history), fuzzy-matches Contacts by approximate name to return send-ready phone numbers, lists named group chats with their chat IDs, sends iMessage with SMS/RCS fallback for eligible recipients, checks likely iMessage reachability before sending, finds attachments by date/sender/MIME type, returns small images inline (converting HEIC to PNG) or a local path for larger and non-image files, and diagnoses Messages/Contacts database permission issues from inside the MCP client.
Its eleven tools: tool_get_recent_messages, tool_fuzzy_search_messages, tool_find_contact, tool_get_chats, tool_search_attachments, tool_get_attachment, tool_check_imessage_availability, tool_check_db_access, tool_check_contacts and tool_check_addressbook are all read-only; only tool_send_message has a real side effect - sending a message through Messages.app. It also exposes two MCP resources, messages://recent/{hours} and messages://contact/{contact}/{hours}.
For recipients, E.164 phone numbers are most reliable; national-format numbers are expanded to E.164 using the Mac's configured region (overridable via MAC_MESSAGES_REGION), and email addresses, contact names or an ambiguous-match contact:N selection are also accepted. Group messages go through tool_get_chats for the chat ID, then tool_send_message with group_chat=true. Attachment access is deliberately three steps: message reads add compact markers like [attachments: #42 image/jpeg (invitation.jpg)], tool_search_attachments searches metadata without loading file contents, and tool_get_attachment fetches one selected item - inline for images up to 5 MB (HEIC converted to PNG), a local path for larger files, PDFs, video or audio, with stickers and link-preview payloads filtered out.
On privacy and security: Messages and Contacts SQLite connections use read-only mode and query_only; the server does not upload, mirror or maintain its own message archive; Messages/Contacts-derived tool output is structurally neutralized (embedded newlines, ASCII controls and invisible/bidi characters cannot form extra transcript lines and are shown as escapes) and returned inside an explicit <untrusted-mcp-output> block - which is labeling, not an anti-injection guarantee, since third-party iMessage/SMS content can still attempt prompt injection and the client must not treat it as authorization or instructions; attachment bytes are returned only after an explicit fetch, size-limited for inline images; and sending is isolated in tool_send_message, escapes AppleScript inputs, uses a bounded execution timeout, and performs no human confirmation itself - the calling MCP client must gate sends. Full Disk Access, required to read messages, is broader than Messages access alone, so it should only be granted to trusted MCP clients.
When to use - and when NOT to
Use it when an MCP client needs to read, search or send macOS Messages/iMessage conversations. This server is macOS-only; reading requires Full Disk Access, and sending requires a Mac signed into Messages plus permission for the launching app to automate Messages.app.
Capabilities
Read recent or filtered messages, fuzzy-search message text over a time window, fuzzy-match contacts to phone numbers, list group chats, send iMessage with SMS/RCS fallback, check iMessage reachability, search and fetch attachments (with inline images and HEIC-to-PNG conversion), and diagnose Messages/Contacts database access.
How to install
Install uv (brew install uv), grant Full Disk Access to the launching app under System Settings, then add the server with the command uvx mac-messages-mcp - for example in Claude Desktop's config as {"mcpServers": {"mac-messages": {"command": "uvx", "args": ["mac-messages-mcp"]}}}, or via claude mcp add --transport stdio --scope user mac-messages -- uvx mac-messages-mcp for Claude Code.
It is MIT licensed. The bundled Dockerfile is for package/catalog validation only - a Linux container has no access to macOS TCC permissions or Messages.app, so Docker is not a supported way to actually read or send messages on the host Mac.
Common failure modes: a spawn uvx ENOENT error means the GUI app can't see the shell's Homebrew path, fixed by using the full path from which uvx as the MCP command; Operation not permitted or an unreadable database means Full Disk Access needs to be granted to the actual launching app (the terminal, or the desktop/IDE app), not just to Messages.app; and reading working while sending fails is usually the launching app missing Automation permission for Messages.app under System Settings.
Who it's for
It suits users of Claude, Codex, Cursor or VS Code on a Mac who want their AI client to read, search or send Messages/iMessage conversations directly.
Source README
Mac Messages MCP
Read, search, and send macOS Messages from any local MCP client.
Quick start · Available tools · Agent skill · Command-line interface · Security · Changelog
Use Claude, Codex, Cursor, VS Code, or any local MCP client to search, read, and send messages through the macOS Messages app.
Mac Messages MCP runs locally on your Mac. It opens the Messages and Contacts databases read-only, returns only the data a client asks for, and uses Messages.app automation only when the client explicitly calls the send tool.
How it works
flowchart LR
A[MCP client] -->|stdio| B[mac-messages-mcp]
B --> C{Tool call}
C -->|read tools| D[(chat.db read-only)]
C -->|read tools| E[(AddressBook read-only)]
C -->|send tools| F[Messages.app]
F --> G[Recipient]
D --> H[Untrusted-output boundary]
E --> H
H --> A
Everything the client reads comes from the local SQLite databases, opened read-only. Everything that leaves the machine goes out through Messages.app, under an explicit send tool call. Message- and contact-derived text is fenced as untrusted data on the way back to the model.
Capabilities
| Area | What Mac Messages MCP provides |
|---|---|
| Reading | Recent messages across all conversations or filtered by contact, group chat, date range, or unread state; paging with limit/offset and forward-cursor reads |
| Conversations | Every conversation (1:1, business, and group) with kind, message and unread counts, last activity, and a blocking wait for new messages |
| Search | Fuzzy message-body search across a time window or all history, scoped to a contact or conversation |
| Context | Per-message service (iMessage/SMS/RCS), unread and delivery state, tapbacks, and threaded replies |
| Contacts | Fuzzy contact lookup by approximate name, send-ready phone numbers, and contact creation |
| Group chats | Named group chat discovery with stable chat IDs reused for reads and sends |
| Sending | iMessage with SMS/RCS fallback, file attachments, optional human approval through MCP elicitation, and iMessage reachability checks |
| Scheduling | Queue a message for later delivery while the server process stays alive |
| Attachments | Metadata search, best-effort text/PDF content search, inline images (HEIC converted to PNG), and local paths for larger or non-image files |
| Diagnostics | Messages and Contacts database permission checks from inside the MCP client |
| Privacy | Read-only SQLite access, no message archive of its own, and a structural untrusted-output boundary |
Quick start
1. Install uv
brew install uv
Confirm that the launcher is available:
uvx --version
Python 3.10 or newer is required. uvx can provision a compatible Python and
installs Mac Messages MCP in an isolated environment, so you do not need to
create a virtual environment first.
2. Grant macOS permissions
Open System Settings → Privacy & Security → Full Disk Access and enable the app that will launch the MCP server:
- Claude Desktop, Cursor, VS Code, or the ChatGPT desktop app when configured in that app
- Terminal, iTerm2, Ghostty, or another terminal when using Claude Code or Codex CLI from that terminal
Quit and reopen the app after changing Full Disk Access. On the first contact lookup or send, macOS may separately ask for access to Contacts or permission to control Messages. Allow those prompts.
Also make sure Messages.app is open, signed in, and already able to send a normal message.
3. Add the server to your MCP client
The server command is the same everywhere:
uvx mac-messages-mcp
Choose your client below.
Claude Desktop
Open Claude → Settings → Developer → Edit Config, then add:
{
"mcpServers": {
"mac-messages": {
"command": "uvx",
"args": ["mac-messages-mcp"]
}
}
}
Preserve any other servers already in claude_desktop_config.json, save the
file, and restart Claude Desktop.
Claude Desktop also supports installable .mcpb extensions. See
Build the Claude Desktop extension if you
want to package this repository as one.
Claude Code
Add it once at user scope so it is available in every project:
claude mcp add --transport stdio --scope user mac-messages -- uvx mac-messages-mcp
Verify it:
claude mcp get mac-messages
Inside Claude Code, run /mcp to inspect the connection and tools.
Codex CLI, Codex IDE extension, and ChatGPT desktop app
Codex clients on the same Mac share MCP configuration. Add the server with:
codex mcp add mac-messages -- uvx mac-messages-mcp
Then verify it:
codex mcp list
You can also add it directly to ~/.codex/config.toml:
[mcp_servers.mac-messages]
command = "uvx"
args = ["mac-messages-mcp"]
Restart the desktop app or IDE extension after changing the configuration. In
Codex CLI, use /mcp to view the active server.
Cursor
Or open Cursor Settings → Tools & MCP → New MCP Server and use:
{
"mcpServers": {
"mac-messages": {
"command": "uvx",
"args": ["mac-messages-mcp"]
}
}
}
Restart the server from Cursor's MCP settings after saving.
VS Code / GitHub Copilot
Open the Command Palette and run MCP: Add Server. Choose Command
(stdio), enter uvx as the command, add mac-messages-mcp as the argument,
and install it globally.
Or add it from a terminal:
code --add-mcp '{"name":"mac-messages","command":"uvx","args":["mac-messages-mcp"]}'
The equivalent user or workspace mcp.json entry is:
{
"servers": {
"mac-messages": {
"type": "stdio",
"command": "uvx",
"args": ["mac-messages-mcp"]
}
}
}
Other stdio MCP clients
Use this generic server definition:
{
"command": "uvx",
"args": ["mac-messages-mcp"]
}
If a GUI client reports that uvx cannot be found, run which uvx in Terminal
and replace "uvx" with the returned absolute path. Homebrew commonly installs
it at /opt/homebrew/bin/uvx on Apple silicon and /usr/local/bin/uvx on Intel
Macs.
4. Verify the connection
Ask your client to call tool_check_db_access, then tool_check_addressbook.
Once both succeed, try prompts such as:
Show me my messages from the last two hours.
Find messages from Carter about dinner in the last 30 days.
Find PDFs sent to me this month, but do not open any yet.
Find Jordan in my contacts and draft a message saying I am running 10 minutes
late. Do not send it until I confirm.
The first uvx launch can take longer while it downloads and caches Python
dependencies.
5. Optional: set the phone number region
Phone numbers written in national format (06 39 98 00 01, (415) 555-1234)
have to be expanded to E.164 before they can be matched against the Messages
database, and that expansion needs to know which country they belong to. The
server reads your Mac's own region setting for this, so on a correctly
configured Mac there is nothing to do.
Set MAC_MESSAGES_REGION to an ISO 3166-1 alpha-2 code when your
numbers belong to a different region than your Mac is configured for - a French
SIM on a Mac set to en_US, say:
{
"mcpServers": {
"mac-messages": {
"command": "uvx",
"args": ["mac-messages-mcp"],
"env": { "MAC_MESSAGES_REGION": "FR" }
}
}
}
For Claude Code:
claude mcp add --transport stdio --scope user \
--env MAC_MESSAGES_REGION=FR \
mac-messages -- uvx mac-messages-mcp
The region is resolved once at startup, so restart the server after changing
it. Resolution order: MAC_MESSAGES_REGION, then the macOS AppleLocale
preference, then LC_ALL / LC_CTYPE / LANG, then US. Numbers already
written in E.164 (+33639980001) are never reinterpreted and need none of
this.
Available tools
| Tool | Purpose | Side effect |
|---|---|---|
tool_get_recent_messages |
Read recent messages, filtered by contact, group chat ID, date range, or unread state; page with limit/offset |
Read-only |
tool_fuzzy_search_messages |
Search message bodies by approximate text match, scoped to a contact or chat; defaults to 30 days, or hours=0 |
Read-only |
tool_list_conversations |
List every conversation with kind, message count, unread count, and last activity | Read-only |
tool_wait_for_new_messages |
Block until a message newer than a ROWID cursor arrives, or the timeout expires | Read-only |
tool_find_contact |
Fuzzy-match a name in Contacts and return phone numbers | Read-only |
tool_get_chats |
List named group chats and their identifiers | Read-only |
tool_search_attachments |
Find attachment metadata by date, contact, MIME type, and limit | Read-only |
tool_search_attachment_contents |
Search inside attachment text/PDF contents by date, contact, and MIME type | Read-only |
tool_get_attachment |
Fetch one attachment by ID, inline when supported or as a local path | Read-only |
tool_check_imessage_availability |
Check likely iMessage availability for a phone number or email | Read-only |
tool_check_db_access |
Diagnose access to ~/Library/Messages/chat.db |
Read-only |
tool_check_contacts |
Return a contact count and a small sample | Read-only |
tool_check_addressbook |
Diagnose Contacts/AddressBook database access | Read-only |
tool_send_message |
Send one direct or group message through Messages.app, optionally with file attachments and elicitation approval | Sends a real message |
tool_create_contact |
Create one Contacts.app entry with a phone number | Changes Contacts |
tool_schedule_message |
Queue a message for later delivery while this server process runs | Sends a real message later |
tool_list_scheduled_messages |
List the in-process scheduled-message queue and its status | Read-only |
tool_cancel_scheduled_message |
Cancel one pending scheduled message | Read-only |
Three MCP prompts ship with the server: triage_unread_messages,
summarize_recent_messages, and draft_reply. They are starting points that
tell the agent which read tools to call, and they never send anything.
The server also exposes two MCP resources:
messages://recent/{hours}messages://contact/{contact}/{hours}
Message metadata, paging, and cursors
Reads append compact tags when the message row carries the data, so output stays unchanged for old or partial databases:
[2026-10-01 09:14:02] Alice [iMessage] [unread]: running late, sorry
[2026-10-01 09:15:40] You [iMessage] [not delivered]: no worries
[2026-10-01 09:15:55] Alice [iMessage] [tapback: liked]: (reaction)
[2026-10-01 09:16:10] Bob [SMS] [reply]: got it
tool_get_recent_messages closes with a note when a page fills up, naming the
next offset to use. Passing since_rowid switches it to a forward cursor read
(oldest first) that ignores the hours window, which is the pattern
tool_wait_for_new_messages polls on.
What it deliberately does not do
Messages.app automation does not expose these, so the server refuses rather than pretending:
- sending tapbacks, message effects, or a subject line (only text and file attachments can be sent)
- creating a group chat, adding or removing participants, or attaching files to a group chat
- editing, unsending, deleting, or marking a message read, and saving drafts
- editing or merging an existing Contacts card (
tool_create_contactcreates a new one) - scheduled sends rely on the server process staying alive; nothing is persisted, so a scheduled message is lost if the client disconnects first
Agent skill
The repository ships an agent skill that tells an agent when and how to call each tool: checking access first, listing conversations, resolving a recipient or group chat, searching message text or attachment contents, paging attachments, confirming a send, scheduling a later send, and treating message-derived output as untrusted data. Claude Code discovers it automatically when you work in this repository.
To use it from another project, copy the skill directory into that project's
.claude/skills/, or into ~/.claude/skills/ to make it available everywhere:
mkdir -p ~/.claude/skills
cp -R .claude/skills/mac-messages ~/.claude/skills/
Working with contacts, chats, and attachments
Recipients
For direct messages, E.164 phone numbers are the most reliable format:
+14155551234
Numbers written in national format work too. They are expanded to E.164 using
the region your Mac is configured for, so (415) 555-1234 becomes
+14155551234 on a US Mac and 06 39 98 00 01 becomes +33639980001 on a
French one. Set MAC_MESSAGES_REGION to an ISO 3166-1 alpha-2 code
(MAC_MESSAGES_REGION=GB) when your numbers belong to a different region than
your Mac does. Numbers already in E.164 are never reinterpreted.
The server also accepts email addresses, contact names, and contact:N
selections returned after an ambiguous contact search.
For a group conversation, call tool_get_chats, pass its chat ID to
tool_send_message, and set group_chat=true. Use the same ID as chat_id in
tool_get_recent_messages to read that conversation.
Attachments
Attachment access is deliberately split into three steps:
- Message reads and searches add compact markers such as
[attachments: #42 image/jpeg (invitation.jpg)]. tool_search_attachmentssearches metadata without loading file contents.tool_get_attachmentfetches one selected attachment.
Images up to 5 MB are returned inline by default. HEIC images are converted to
PNG. Larger images, PDFs, video, and audio are returned as local filesystem
paths so the MCP client can decide whether to open them. Stickers, link-preview
payloads, and .pluginPayloadAttachment containers are filtered out.
Architecture
Mac Messages MCP is a small Python package with one job per module:
mac_messages_mcp/
server.py FastMCP server: tools, prompts, resources, stdio transport
cli.py mac-messages-cli terminal interface
messages.py chat.db reads and AppleScript sends for Messages.app
content.py Best-effort text/PDF attachment content search
phone.py E.164 normalization against the Mac's configured region
scheduler.py In-process scheduled-send queue
untrusted.py Structural neutralization of Messages/Contacts-derived text
scripts/
build_mcpb.py Builds the Claude Desktop .mcpb extension
bump_version.py Keeps version metadata in sync across release files
tests/ Pytest suite over temporary databases and mocked AppleScript
main.py Stdio entry point for MCP clients
manifest.json MCPB manifest for the Claude Desktop extension
Reads go straight to chat.db and the AddressBook database; sends go through
AppleScript into Messages.app. Model-facing payloads pass through
untrusted.py before they reach the client.
Troubleshooting
uvx or spawn uvx ENOENT
The GUI app cannot see your shell's Homebrew path. Run:
which uvx
Use that full path as the MCP command, then restart the client.
Operation not permitted, unable to open database file, or no messages
Grant Full Disk Access to the app that launches the server, not just to
Messages.app. Completely quit and reopen the launcher afterward, then call
tool_check_db_access again.
For Claude Code or Codex CLI, the launcher is normally your terminal. For a desktop or IDE integration, it is normally Claude Desktop, Cursor, VS Code, or the ChatGPT desktop app itself.
Contacts are empty or contact lookup fails
Allow the launching app to access Contacts if macOS prompts. Confirm Full Disk
Access, restart the app, and call tool_check_addressbook followed by
tool_check_contacts.
If contacts are listed but their numbers carry the wrong country code, the
server is expanding your national-format numbers against the wrong region. Set
MAC_MESSAGES_REGION to the right ISO 3166-1 alpha-2 code and restart the
server.
Reading works but sending fails
- Open Messages.app and send a message manually to confirm the account and recipient work.
- Check System Settings → Privacy & Security → Automation and allow the launching app to control Messages.
- Prefer an E.164 number such as
+14155551234for a direct recipient. - Use
tool_check_imessage_availabilityto inspect the likely route.
An attachment is listed but cannot be opened
Messages may retain database metadata after macOS has offloaded the file. Open
the conversation in Messages.app and download the attachment, then retry
tool_get_attachment.
The server appears to hang when run in Terminal
That is normal for an MCP stdio server: it waits for protocol input from a client. Use your client's MCP status view, or launch the MCP Inspector:
yarn dlx @modelcontextprotocol/inspector uvx mac-messages-mcp
Install as a standalone tool
MCP clients can launch the package directly with uvx; a permanent installation
is optional.
uv tool install mac-messages-mcp
mac-messages-mcp
Upgrade or remove it with:
uv tool upgrade mac-messages-mcp
uv tool uninstall mac-messages-mcp
Python API
The MCP server is the primary interface, but the package also exports its core read/send functions:
from mac_messages_mcp import get_recent_messages, send_message
recent = get_recent_messages(hours=48)
print(recent)
result = send_message(
recipient="+14155551234",
message="Hello from Mac Messages MCP!",
)
print(result)
These calls use the same macOS permissions and can send real messages.
Command-line interface
The package also installs a mac-messages-cli command, so Messages can be read,
searched, and sent from a terminal without an MCP client. It uses the same local
databases and needs the same macOS permissions as the server; granting Full Disk
Access to your terminal covers both.
uv tool install mac-messages-mcp
mac-messages-cli --help
| Command | Purpose |
|---|---|
recent |
Recent messages, optionally filtered by contact or group chat ID. |
search TERM |
Fuzzy-search message text within a time window. |
contact NAME |
Fuzzy-match a name in Contacts and print send-ready numbers. |
contacts |
Contact count plus a sample of AddressBook entries. |
chats |
Named group chats and their identifiers. |
attachments |
Attachment metadata by date range, contact, and MIME type. |
attachment ID |
One attachment's metadata and local path, with optional --save. |
send |
Send one message through Messages.app. |
check |
Diagnose Messages/AddressBook access and iMessage reachability. |
# Last 48 hours, or one contact, or one group chat
mac-messages-cli recent -n 48
mac-messages-cli recent --contact "Jordan"
mac-messages-cli recent --chat chat721054478304420871
# Fuzzy search: last 30 days by default, or all history with -n 0
mac-messages-cli search "dinner" -t 0.7
mac-messages-cli search "dinner" -n 0
# Contacts and group chats
mac-messages-cli contact "Jordan"
mac-messages-cli chats
# Attachments: search metadata, then fetch or save one file
mac-messages-cli attachments --since 2026-09-01 --mime image/ --limit 20
mac-messages-cli attachment 42 --save ~/Desktop/invitation.jpg
# Permissions and iMessage reachability
mac-messages-cli check --recipient +14155551234
# Sending prompts for confirmation unless --yes is passed
mac-messages-cli send +14155551234 "Running 10 minutes late."
mac-messages-cli send asks for y/N confirmation before sending. When stdin is
not a terminal it refuses to send unless --yes is passed, so an unattended
script cannot send a message by accident. Commands exit non-zero when a lookup
fails, so the output is safe to branch on in scripts.
Development
git clone https://github.com/carterlasalle/mac_messages_mcp.git
cd mac_messages_mcp
uv sync --frozen --extra dev
uv run pytest
uv run black --check .
uv run isort --check-only .
uv build
Tests mock AppleScript and use temporary database fixtures; they must never read a contributor's real Messages or Contacts data. See CONTRIBUTING.md for the contribution checklist and VERSIONING.md for releases.
Build the Claude Desktop extension
The repository includes an MCPB manifest.json and a build script that can
bundle an architecture-specific uv binary:
yarn global add @anthropic-ai/mcpb
uv run python scripts/build_mcpb.py
For an Intel build:
uv run python scripts/build_mcpb.py --arch x86_64
Install the generated .mcpb from Claude Desktop → Settings → Extensions →
Advanced settings → Install Extension…. A bundled extension still needs
network access on first launch to download Python and the package dependencies.
Use --no-bundle to package against the system uv, or run
uv run python scripts/build_mcpb.py --help for every option.
Docker
The included Dockerfile is for package and catalog validation. A Linux container cannot access macOS TCC permissions or automate Messages.app, so Docker is not a supported way to read or send messages on the host Mac.
Documentation
| Document | Purpose |
|---|---|
| Agent skill | When and how an agent should call each tool |
| Contributing | Development workflow, checks, and pull-request standards |
| Security | Private vulnerability reporting and the trust model |
| Versioning | Release and version metadata process |
| Changelog | User-facing changes by release |
scripts/build_mcpb.py
#!/usr/bin/env python3
# Copyright (c) 2023 Carter Lasalle
"""Build script for the Mac Messages MCP Claude Desktop extension (.mcpb).
Vendors a `uv` binary into the bundle so the packaged extension runs on
machines that don't have `uv` installed, then runs `mcpb pack`.
Usage:
python scripts/build_mcpb.py [--arch arm64|x86_64]
[--uv-version X.Y.Z] [--no-bundle]
python scripts/build_mcpb.py --help
Options:
--arch Target architecture (defaults to the host architecture).
--uv-version uv release to vendor (default: pinned known-good version).
--no-bundle Pack against the system `uv` without vendoring a binary.
The vendored `uv` binary is architecture specific, so build one .mcpb per
architecture you need to support. On first launch the bundled `uv` still
downloads Python and dependencies, so network access is required once.
"""
import hashlib
import http.client
import json
import platform
import shutil
import subprocess
import sys
import tarfile
import tempfile
from pathlib import Path
from urllib.parse import urljoin, urlparse
# Pinned uv release for reproducible builds; override with --uv-version.
DEFAULT_UV_VERSION = "0.11.19"
UV_DOWNLOAD_PREFIX = "https://github.com/astral-sh/uv/releases/download/"
UV_DOWNLOAD_URL = UV_DOWNLOAD_PREFIX + "{version}/{asset}.tar.gz"
_ALLOWED_DOWNLOAD_HOSTS = frozenset(
{
"github.com",
"objects.githubusercontent.com",
"github-releases.githubusercontent.com",
"release-assets.githubusercontent.com",
},
)
_DOWNLOAD_TIMEOUT_SECONDS = 60
_MAX_DOWNLOAD_REDIRECTS = 8
# Map architecture aliases to uv's macOS release asset names.
ARCH_ASSETS = {
"arm64": "uv-aarch64-apple-darwin",
"aarch64": "uv-aarch64-apple-darwin",
"x86_64": "uv-x86_64-apple-darwin",
"intel": "uv-x86_64-apple-darwin",
}
BIN_DIR = Path("bin")
MANIFEST_PATH = Path("manifest.json")
BUNDLED_COMMAND = "${__dirname}/bin/uv"
def print_help():
"""Print help information."""
print(__doc__)
sys.exit(0)
def resolve_asset(arch):
"""Map an architecture alias to uv's release asset name."""
key = (arch or platform.machine()).lower()
asset = ARCH_ASSETS.get(key)
if asset is None:
valid = ", ".join(sorted(set(ARCH_ASSETS)))
print(f"Error: unsupported arch '{key}'. Choose from: {valid}")
sys.exit(1)
return asset
def download(url):
"""Download a GitHub uv release URL and return the raw bytes.
Raises
------
ValueError
If ``url`` is not a pinned uv release URL.
"""
if not url.startswith(UV_DOWNLOAD_PREFIX):
msg = f"refusing to download untrusted URL: {url}"
raise ValueError(msg)
print(f"Downloading {url}")
return _https_get_allowlisted(url)
def _https_get_allowlisted(url):
"""GET `url` over HTTPS, following redirects only to known GitHub hosts.
Raises
------
ValueError
If a URL is not an allowlisted HTTPS GitHub host, the response is not
a redirect or 200, or the redirect limit is exceeded.
"""
current = url
for _ in range(_MAX_DOWNLOAD_REDIRECTS):
parsed = urlparse(current)
if (
parsed.scheme != "https"
or not parsed.hostname
or parsed.hostname not in _ALLOWED_DOWNLOAD_HOSTS
):
msg = f"refusing to download untrusted URL: {current}"
raise ValueError(msg)
path = parsed.path or "/"
if parsed.query:
path = f"{path}?{parsed.query}"
connection = http.client.HTTPSConnection(
parsed.hostname,
timeout=_DOWNLOAD_TIMEOUT_SECONDS,
)
try:
connection.request(
"GET",
path,
headers={"User-Agent": "mac-messages-mcp-mcpb-builder"},
)
response = connection.getresponse()
if response.status in {301, 302, 303, 307, 308}:
location = response.getheader("Location")
response.read()
if not location:
msg = f"redirect without Location from {current}"
raise ValueError(msg)
current = urljoin(current, location)
continue
if response.status != 200:
msg = f"download failed: HTTP {response.status} for {current}"
raise ValueError(
msg,
)
return response.read()
finally:
connection.close()
msg = f"too many redirects downloading {url}"
raise ValueError(msg)
def vendor_uv(asset, version):
"""Download, checksum-verify, and extract the uv binary into bin/uv."""
url = UV_DOWNLOAD_URL.format(version=version, asset=asset)
archive = download(url)
expected = download(url + ".sha256").decode().split()[0]
actual = hashlib.sha256(archive).hexdigest()
if actual != expected:
print("Error: checksum mismatch for uv download")
print(f" expected: {expected}")
print(f" actual: {actual}")
sys.exit(1)
print("Checksum verified")
BIN_DIR.mkdir(exist_ok=True)
uv_path = BIN_DIR / "uv"
with tempfile.TemporaryDirectory() as tmp:
tar_path = Path(tmp) / "uv.tar.gz"
tar_path.write_bytes(archive)
with tarfile.open(tar_path) as tar:
source = tar.extractfile(f"{asset}/uv")
uv_path.write_bytes(source.read())
uv_path.chmod(0o755)
print(f"Vendored uv {version} ({asset}) -> {uv_path}")
def mcpb_cli():
"""Return the command used to invoke the mcpb CLI."""
if shutil.which("mcpb"):
return ["mcpb"]
return ["npx", "--yes", "@anthropic-ai/mcpb"]
def pack(bundle):
"""Run `mcpb pack`, temporarily pointing the manifest at the bundled uv.
The tracked manifest.json is always restored afterwards so the default
system-uv flow stays unchanged.
"""
original = MANIFEST_PATH.read_text(encoding="utf-8")
try:
if bundle:
manifest = json.loads(original)
manifest["server"]["mcp_config"]["command"] = BUNDLED_COMMAND
MANIFEST_PATH.write_text(
json.dumps(manifest, indent=2) + "\n",
encoding="utf-8",
)
print(f"manifest command -> {BUNDLED_COMMAND}")
subprocess.run([*mcpb_cli(), "pack"], check=True, shell=False)
finally:
MANIFEST_PATH.write_text(original, encoding="utf-8")
if bundle:
print("Restored manifest.json")
def main():
args = sys.argv[1:]
if any(a in {"-h", "--help", "help"} for a in args):
print_help()
arch = None
uv_version = DEFAULT_UV_VERSION
bundle = True
i = 0
while i < len(args):
arg = args[i]
if arg == "--arch":
i += 1
if i >= len(args):
print("Error: --arch requires a value")
sys.exit(1)
arch = args[i]
elif arg == "--uv-version":
i += 1
if i >= len(args):
print("Error: --uv-version requires a value")
sys.exit(1)
uv_version = args[i]
elif arg == "--no-bundle":
bundle = False
else:
print(f"Error: unknown argument '{arg}'")
print(
"Usage: python scripts/build_mcpb.py [--arch arm64|x86_64] "
"[--uv-version X.Y.Z] [--no-bundle]",
)
sys.exit(1)
i += 1
if sys.platform != "darwin":
print("Warning: mac-messages-mcp targets macOS; building on a non-macOS host.")
if bundle:
asset = resolve_asset(arch)
vendor_uv(asset, uv_version)
else:
print("Packing against system uv (no vendored binary).")
pack(bundle)
print("Done.")
if __name__ == "__main__":
main()
FAQ
Common questions
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.