Install
$ agentstack add mcp-54yyyu-pyapple-mcp ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
PyApple MCP Tools
[](https://pepy.tech/projects/pyapple-mcp) [](LICENSE) [](https://modelcontextprotocol.io)
A Python implementation of Apple-native tools for the Model Context Protocol (MCP), 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
--jsonoutput 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)
# 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
- Install pyapple-mcp:
``bash pip install pyapple-mcp ``
- Configure Claude Desktop by editing
~/Library/Application Support/Claude/claude_desktop_config.json:
``json { "mcpServers": { "pyapple": { "command": "pyapple-mcp" } } } ``
- 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.
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:
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 `, every tick cleared |
So append, rename, replace and setBody refuse any note carrying that content, name what it holds, and change nothing. There is deliberately no override flag: a flag saying "yes, lose the images" would be set by a model that has never seen the note, on behalf of someone who has, and the loss is invisible afterwards — the attachment does not go to the Trash, it is gone.
On the development machine 0 of 56 existing notes pass that gate — 40 carry an attachment, 36 an image, 19 a heading, 13 a link, and 6 are password protected. That is the honest number, and it is why the refusal is worth documenting rather than discovering. Notes this tool creates always pass, so create then append works as you would expect.
create and delete are unaffected by that gate: neither reads a body back.
create has a refusal of its own, though. It used to make any folder it was handed, silently, so a misspelled folder name did not fail — it started a second folder and filed the note there, reporting success. It now refuses a folder that does not exist and names the ones that do. notes folders is the listing that lets you check first, and notes folder-create is how you make one on purpose; the tool's own default folder is the one exemption, since a caller that named no folder cannot be told the folder it did not ask for is missing.
The refusal is the right way round because of what it costs to be wrong either way. Refusing when a new folder was genuinely wanted costs one further call to folder-create, immediately, with the error naming what the folder could have been. Creating on a typo costs a folder in your sidebar, and two more calls — notes move to file the note where it belonged and folder-delete to remove what the typo made.
notes folder-delete destroys what is in it
Every other delete in this package is recoverable from a Trash or a Recently Deleted. This one is not, and that is measured rather than assumed: a disposable folder holding two notes was deleted, the trash's own note count was 3 before and 3 after, and a search of every note — which does reach the trash, since the same query found a trashed note by name in the same run — matched 0 of the two deleted bodies. The notes are gone, not filed away.
So the refusals are the feature, and there are four of them:
- A folder holding notes is refused unless you pass
--delete-notes Nwith
the folder's exact count. N is re-checked against the live store, so a count that has moved since you read it refuses rather than proceeds. You cannot pass the flag without having counted first, and that interlock is the only thing between a mistyped folder name and an unrecoverable loss.
- A folder with subfolders is refused outright, with no override at all.
Deleting a container leaves the app in a state where id of that container raises, and there is no argument that gets past this.
- The trash and any account's default folder are refused for what they are.
There is one of each per account and nothing here needs to remove them.
- The result carries
may_return: Truerather than a bare success, because
a synced account may put the folder back. Measured on both account types on one machine: an Exchange folder was listed again inside 15 seconds and was still there five minutes later, and iCloud resurrection has been reproduced five times — but neither reproduces every time, which is why the caveat is unconditional rather than naming an account. The script re-queries after deleting and reports what it saw, so what you get is what the store said, not what the command intended.
The message says only what happened. Delete an empty folder and it warns that a synced account may restore it and stops there; the sentence about notes not being in Recently Deleted appears only when notes were actually destroyed, and notes_recoverable is null rather than false when there were none to recover.
pyapple notes folders # read the count first
pyapple notes folder-delete "Scratch" --delete-notes 2
Four of seven maps operations are permanently impossible
Maps.app ships no scripting dictionary — there is no .sdef anywhere in it — and Favourites and Guides have no public API and no readable on-disk copy; the data is CloudKit-backed and opaque. So save, listGuides, createGuide and addToGuide cannot be implemented at all.
They report failure and hand back a durable maps.apple.com link instead, which is the part you can actually keep
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: 54yyyu
- Source: 54yyyu/pyapple-mcp
- License: MIT
- Homepage: https://stevenyuyy.com/pyapple-mcp/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.