# Pyapple Mcp

> MCP server and CLI for seven macOS apps — Messages, Mail, Notes, Calendar, Reminders, Contacts and Maps. Native frameworks, honest truncation.

- **Type:** MCP server
- **Install:** `agentstack add mcp-54yyyu-pyapple-mcp`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [54yyyu](https://agentstack.voostack.com/s/54yyyu)
- **Installs:** 0
- **Category:** [Productivity](https://agentstack.voostack.com/c/productivity)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [54yyyu](https://github.com/54yyyu)
- **Source:** https://github.com/54yyyu/pyapple-mcp
- **Website:** https://stevenyuyy.com/pyapple-mcp/

## Install

```sh
agentstack add mcp-54yyyu-pyapple-mcp
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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)](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 ``, 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 N` with
  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: True`** rather 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.

```bash
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](https://github.com/54yyyu)
- **Source:** [54yyyu/pyapple-mcp](https://github.com/54yyyu/pyapple-mcp)
- **License:** MIT
- **Homepage:** https://stevenyuyy.com/pyapple-mcp/

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** yes
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-54yyyu-pyapple-mcp
- Seller: https://agentstack.voostack.com/s/54yyyu
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
