# PyApple MCP Tools [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyapple-mcp?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/pyapple-mcp) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![MCP](https://img.shields.io/badge/MCP-compatible-purple.svg)](https://modelcontextprotocol.io) A Python implementation of Apple-native tools for the [Model Context Protocol (MCP)](https://modelcontextprotocol.com/docs/mcp-protocol), providing seamless integration with macOS applications. There are two front ends over the same integrations: an MCP server for Claude Desktop and other MCP clients, and `pyapple`, a command line for driving the same ten tools from a shell. They share one permission policy, so what you allow in one you have allowed in both. **88 operations across ten tools.** They are listed in full under [The tools](#the-tools), and what they refuse to do is under [Limits worth knowing](#limits-worth-knowing) — read that before you rely on any of them. If you are an agent about to drive the CLI rather than a person reading about it, `.claude/skills/pyapple/SKILL.md` in this repository is the operator guide: which call to make first, how to read the exit status, and which refusals to route around versus report. ## Features - **Messages**: find conversations, read a thread whole, search message text, list what a thread was sent, and send — text or a file. Reads go through `chat.db` directly and are scoped by *chat*, so a group thread comes back as the group rather than as one participant's share - **Notes**: search, list, read, create, append, rename and rewrite — with a gate that **refuses** any note whose content a round trip would destroy — plus filing a note into another folder, and folders themselves: list them, create one, rename one, delete one. `create` refuses a folder name that does not exist rather than making a folder out of a typo, and the listing is how you find out what does; `folder-delete` refuses a folder holding notes unless you pass their exact count, because what it deletes does not go to Recently Deleted - **Contacts**: search by name, go the other way from a number or address to a person, and add, correct or delete a card. Reads and writes both go through the Contacts framework; nothing launches Contacts.app - **Mail**: unread, header search, **body** search, read one message whole by id, a whole **thread** in order, what is **attached** and whether the bytes are here, **coverage** — what each account can actually answer about — send, reply, forward, the drafted form of each of those three, mark read, file, delete, and list mailboxes and accounts. Reads come from the local Envelope Index rather than through the app - **Reminders**: list, search, create, update, complete, uncomplete and delete, plus creating and deleting the lists themselves, through EventKit - **Calendar**: list and search occurrences of recurring events, list calendars, **coverage** — what range of events each calendar actually holds — **find free gaps rather than events**, create with repeats, alarms and invitees, reschedule, move and delete - **Maps**: location search and directions through MapKit — no window, no Automation grant, and Maps never launches. Favourites and Guides are not available at all and say so - **Shortcuts**: list the user's own automations, list the library's folders, describe one, open it in the editor, run it. Through `/usr/bin/shortcuts` — a subprocess and **not** an Apple Event, so it costs no Automation grant and launches nothing - **Presence**: whether now is a moment to interrupt — Focus mode, the frontmost app, idle seconds, whether the screen is locked. Read-only, and structurally so: **there is no setter and there must not be one** - **Doctor**: why a tool just failed. Which macOS grants this process holds, per tool, and where to click for the missing ones. Nothing it does can launch an app or raise a permission prompt - **Command line**: the same ten tools from a shell, with `--json` output that says how much of the answer came back, and an exit status you can branch on — see [Command Line](#command-line) - **Permissions**: one policy string, honoured identically by the server and the CLI, and an optional [two-phase gate](#confirming-a-send-before-it-goes) that makes any send show what it would send first — see [Permission Policy](#permission-policy) ## Quick Installation ### Automated Setup (Recommended) ```bash # Install pyapple-mcp pip install pyapple-mcp # Run the setup helper to configure Claude Desktop pyapple-mcp-setup ``` The setup helper will: - Find your pyapple-mcp installation - Locate your Claude Desktop config file - Automatically add the configuration - Display helpful setup information ### Manual Installation 1. **Install pyapple-mcp**: ```bash pip install pyapple-mcp ``` 2. **Configure Claude Desktop** by editing `~/Library/Application Support/Claude/claude_desktop_config.json`: ```json { "mcpServers": { "pyapple": { "command": "pyapple-mcp" } } } ``` 3. **Restart Claude Desktop** to load the new configuration. ### What gets installed Three commands land on your `PATH`: | Command | What it is | | --- | --- | | `pyapple` | the command line — the ten tools from a shell | | `pyapple-mcp` | the MCP server, which is what an MCP client launches | | `pyapple-mcp-setup` | the helper that writes the Claude Desktop config | Nothing else is needed to use the CLI; `pip install pyapple-mcp` is the whole installation. ```bash pyapple --version pyapple --help ``` ## Usage Examples Through the MCP server you ask in plain language; through the CLI you type the operation. The first two headings below are the former, the third is the latter. ### Basic Commands ``` Can you send a message to my sister saying "Hello from Claude!"? ``` ``` Find all notes about "AI research" and summarize them ``` ``` Create a reminder to "Buy groceries" for tomorrow at 5pm ``` ``` When am I free for an hour on Thursday? ``` ``` Get directions from "Apple Park" to "San Francisco Airport" ``` ### Advanced Workflows You can chain commands together for complex workflows: ``` "Find the email with the booking reference in it, and put the reference in my trip note" ``` ### From a shell The same operations are available without an MCP client at all: ```bash pyapple messages conversations --query "book club" pyapple mail unread --limit 5 --since 2026-08-01 pyapple calendar free --from-date 2026-08-06 --to-date 2026-08-08 --duration 60 pyapple presence snapshot pyapple doctor report ``` See [Command Line](#command-line) below. ## Limits worth knowing Every one of these was measured against the real applications, and each is something you need to know **before** relying on a tool rather than after. They are not bugs waiting to be fixed; where a thing cannot be done, the operation still exists and refuses out loud, because an assistant that has no operation for what it was asked will reach for the nearest one that does exist. ### Almost nothing launches an app any more — but Mail, Messages and Notes still do Reminders, calendar and contacts **writes** moved from Apple Events to EventKit and `CNSaveRequest`; maps search and directions moved to MapKit. Those four tools now need only the privacy grant their reads already held, and the separate *"control Automation"* grant they used to demand is gone. Verified with Calendar, Reminders and Maps all quit: listing calendars, listing reminder lists, finding free time, searching for a place and routing between two of them launched none of the three. What still dispatches Apple Events, and therefore still launches its app and still needs Automation: | Tool | What needs Automation | | --- | --- | | `notes` | **everything**, reads included — `move` and `delete` too | | `mail` | `send`, `reply`, `draft`, `replyDraft`, `forward`, `forwardDraft`, `move`, `delete`, `open`, `markRead`, `mailboxes`, and `unread --mark-read`. `reply`, `replyDraft`, `forward`, `forwardDraft` and anything with `--html` need an **Accessibility** grant on top | | `messages` | `send`, `sendAttachment`, `open` | | `calendar` | `open`, and `create --invitees` | | `reminders` | `open` | | `contacts`, `maps`, `shortcuts`, `presence`, `doctor` | nothing | `mail thread` and `mail attachments` are on the index-and-filesystem side with the other reads: no Apple Event, Mail is not launched, and nothing is marked read. `reminders listCreate` and `listDelete` are EventKit like the rest of that tool. **`shortcuts` is a fourth route into macOS and the cheapest one here.** It goes through `/usr/bin/shortcuts`, a plain subprocess. Measured against the binary Apple ships: `codesign -d --entitlements -` shows no `com.apple.security.automation.apple-events`, and `otool -L` finds no AppleEvents and no Carbon — it reaches its daemon over `mach-lookup`. `shortcuts list` and `shortcuts run ` were both run with Shortcuts.app not running; neither prompted and neither started it. `shortcuts open` is the exception and puts the editor on screen, which is what `calendar open` and `reminders open` cost. Two honest qualifications, because that is easy to over-read. *We* pay no permission; the **shortcut** does — one whose actions touch Contacts or Photos raises those prompts against Shortcuts, and this package cannot enumerate what a shortcut will reach, so it cannot warn you either. And a shortcut containing an interactive action will put UI on screen and block until somebody answers, which is why `run` has a timeout at all. **Notes is the exception that cannot be fixed.** Notes ships no framework API at all, so AppleScript is the only route in and Notes.app launches for every operation including a search and including `folders`. That is a platform limit, not an omission. `mail accounts` came off that list. It used to ask Mail whether each account was enabled; it now reads the same index `coverage` reads and reports what this Mac actually holds per account, which is the consequence a caller cares about where "disabled" was only a cause of it. It falls back to asking Mail only if the index names no accounts at all, which on a machine with mail means the read failed rather than that there are none. `mail coverage` and `mail readMessage` never touch Mail either — index and filesystem only. `maps pin` is the odd one out: it opens a Maps window, but through `open(1)` and a `maps://` URL rather than an Apple Event, so it needs no Automation grant. It opens *behind* what you are working in unless you pass `--show`. The window cannot be closed again from here — measured, `close window 1` blocks until the Apple Event times out and leaves the window where it was — so a pin is a one-way door. ### Notes editing refuses more than it accepts Notes' HTML serialiser and its HTML parser do not agree, so **the body Notes hands you is not a body it will take back**. Every edit here is a whole-body rewrite, because Notes offers nothing else, so a read-modify-write is destructive. Measured, writing a body back unchanged: | Content | What the round trip does to it | | --- | --- | | images | destroyed outright — 2 `` before, 0 after, three clean trials | | attachments | destroyed, even under a pure identity write | | links | `text` comes back as `text`: the text survives, the URL does not | | headings | five input forms of `

` all stored as a bold span | | checklists | ten candidate markups all stored as plain `