macos-mcp-tools
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@macos-mcp-toolscheck my unread emails"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
@hachitogo/macos-mcp-tools
MCP servers for macOS: Apple Mail, Contacts, Notes, Memory, Messages, Calendar Events, and Reminders.
The project uses a hybrid approach: JXA/osascript for macOS app automation, and direct read-only SQLite access where it is faster and more reliable. In practice this matters most for Apple Mail reads, where pure osascript approaches tended to time out on non-trivial queries. Calendar Events and Reminders use a compiled Swift binary (EventKitCLI) that interfaces directly with Apple's EventKit framework.
What It Provides
mail: read/search Apple Mail and perform selected message actionscontacts: read and update Apple Contacts via JXAnotes: read and update Apple Notes via JXAmemory: local SQLite-backed structured memory storemessages: read/search/send Apple Messages (iMessage and SMS)events: read, create, update, and delete Apple Calendar events via EventKitreminders: read, create, update, and delete Apple Reminders with lists, subtasks, tags, recurrence, and location triggers via EventKit
Related MCP server: apple-mcp
Architecture
flowchart LR
Client["MCP Client\nClaude Desktop / OpenCode"] --> CLI["bunx @hachitogo/macos-mcp-tools <subcommand>"]
CLI --> Mail["mail server"]
CLI --> Contacts["contacts server"]
CLI --> Notes["notes server"]
CLI --> Memory["memory server"]
CLI --> Messages["messages server"]
CLI --> Events["events server"]
CLI --> Reminders["reminders server"]
Mail --> MailDB["Apple Mail SQLite\nEnvelope Index"]
Mail --> JXA["JXA / osascript"]
Contacts --> JXA
Notes --> JXA
Memory --> MemoryDB["SQLite in local data dir"]
Messages --> MsgDB["Messages SQLite\nchat.db"]
Messages --> JXA
Events --> EventKitCLI["EventKitCLI\nSwift binary"]
Reminders --> EventKitCLI
EventKitCLI --> EventKit["Apple EventKit\nCalendars + Reminders"]Requirements
macOS 14 (Sonoma) or newer, on Apple Silicon or Intel
Bun 1.0+
Optional:
pdftotextfor PDF attachment text extractionFull Disk Access for the host process (required by the mail server to read
~/Library/Mail/V10/MailData/Envelope Indexand by the messages server to read~/Library/Messages/chat.db)Automation permission for Mail, Contacts, Notes and Messages; macOS prompts on first use of each
Xcode Command Line Tools, only to build the EventKitCLI Swift binary with
bun run build:swift. Installs from npm ship a prebuilt universalbin/EventKitCLIcovering both architectures; a git checkout does not, because the binary is built by CI rather than committed.
Install Bun with Homebrew:
brew install oven-sh/bun/bunOptional dependency for PDF extraction:
brew install popplerQuick Start
1. Install globally (the only supported pattern for MCP hosts)
bun install -g @hachitogo/macos-mcp-tools@latestThen find the absolute path of the installed launcher. You will paste this path into every MCP host config:
echo "$(bun pm bin -g)/macos-mcp-tools"Typical results are /Users/<you>/.bun/bin/macos-mcp-tools when BUN_INSTALL is set, or /Users/<you>/.cache/.bun/bin/macos-mcp-tools when it is not. Verify it runs:
"$(bun pm bin -g)/macos-mcp-tools" --helpDo not use
bunxin an MCP host config. Hosts such as Claude Desktop start all seven servers at the same instant.bunxlinks each launch into one shared temp directory with no lock, so seven concurrentbunxruns corrupt each other'snode_modulesand the servers crash on startup with errors likeCannot find package 'zod-to-json-schema',Failed to link which: EEXIST, orcould not determine executable to run. Warming the cache does not prevent it. A global install has no install step at launch, so the race cannot happen.
Use the absolute path, not a bare command name. GUI hosts do not inherit your shell
PATH, somacos-mcp-toolsorbunalone will often fail with "No executable file" even though they work in Terminal.
2. Configure Claude Desktop
Open Settings → Developer → Edit Config (or edit ~/Library/Application Support/Claude/claude_desktop_config.json while Claude Desktop is fully quit; the app can overwrite edits made while it is running). Replace /Users/<you>/.cache/.bun/bin/macos-mcp-tools with the path from step 1:
{
"mcpServers": {
"apple_mail": { "command": "/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "args": ["mail"] },
"apple_contacts": { "command": "/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "args": ["contacts"] },
"apple_notes": { "command": "/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "args": ["notes"] },
"memory": { "command": "/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "args": ["memory"] },
"apple_messages": { "command": "/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "args": ["messages"] },
"apple_events": { "command": "/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "args": ["events"] },
"apple_reminders": { "command": "/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "args": ["reminders"] }
}
}Quit and reopen Claude Desktop. If a server fails to start, read ~/Library/Logs/Claude/mcp-server-<name>.log; it prints the exact command Desktop ran and anything the server wrote to stderr.
3. Configure Claude Code
One command per server, same launcher path:
for s in mail contacts notes memory messages events reminders; do claude mcp add "apple_$s" -- "$(bun pm bin -g)/macos-mcp-tools" "$s"; done(The memory server ends up named apple_memory with this loop; rename it if you prefer.)
4. Configure OpenCode
{
"mcp": {
"apple_mail": { "type": "local", "command": ["/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "mail"] },
"apple_contacts": { "type": "local", "command": ["/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "contacts"] },
"apple_notes": { "type": "local", "command": ["/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "notes"] },
"memory": { "type": "local", "command": ["/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "memory"] },
"apple_messages": { "type": "local", "command": ["/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "messages"] },
"apple_events": { "type": "local", "command": ["/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "events"] },
"apple_reminders": { "type": "local", "command": ["/Users/<you>/.cache/.bun/bin/macos-mcp-tools", "reminders"] }
}
}5. Check your setup
"$(bun pm bin -g)/macos-mcp-tools" doctordoctor checks everything that has caused "server disconnected" reports: Bun and macOS versions (it fails below macOS 14, which EventKitCLI requires), installed version versus npm, the EventKitCLI binary and whether it matches your CPU, Full Disk Access for Mail and Messages, Calendar and Reminders access, pdftotext, and every macos-mcp-tools entry in your Claude Desktop and Claude Code configs (absolute path, file exists, not launched through bunx). Each problem comes with the fix. Exit code is 1 when something failed, so it works in scripts.
@hachitogo/macos-mcp-tools 0.5.0 doctor
✔ Runtime Bun 1.3.14, macOS 26.5.2, arm64
✔ Package version 0.5.0 (latest on npm)
✔ EventKitCLI binary /Users/you/.cache/.bun/install/global/node_modules/@hachitogo/macos-mcp-tools/bin/EventKitCLI
✔ EventKitCLI architecture arm64 (this Mac is arm64)
✖ Full Disk Access (Mail) cannot read /Users/you/Library/Mail/V10/MailData/Envelope Index: unable to open database file
fix: System Settings > Privacy & Security > Full Disk Access: enable the app that launches the servers (e.g. Claude), then restart it
✖ Claude Desktop: apple_mail launches through bunx; concurrent server startup corrupts its shared temp install
fix: bun install -g @hachitogo/macos-mcp-tools@latest and set "command" to the absolute path from `bun pm bin -g`/macos-mcp-toolsCalendar and Reminders access is granted per launching app, so a warning there from Terminal does not mean the host is affected.
6. Updating
A global install does not update itself. After a new release, clear Bun's cached registry metadata first, then reinstall:
bun pm cache rm && bun install -g @hachitogo/macos-mcp-tools@latestThen quit and reopen the host. The cache clear matters: Bun keeps the package manifest it last saw, so for some minutes after a publish @latest (and even an exact new version) can resolve to the previous release with no warning. Run doctor afterwards; its "Package version" line compares what is installed with npm.
Expect a permission prompt after each update. Calendar and Reminders access is granted to
EventKitCLI, and macOS identifies it by its code signature. See "The EventKitCLI binary" below for why that changes on every release.
The EventKitCLI binary
events and reminders do not use AppleScript. They shell out to bin/EventKitCLI, a small Swift program that talks to Apple's EventKit framework directly, because AppleScript cannot express recurrence rules, alarms, structured locations or attendees.
What ships. GitHub Actions compiles it from swift/EventKitCLI.swift during the release and puts it in the npm tarball, so the binary you install was built from the source in the tagged commit. It is a universal binary (arm64 + x86_64) with a macOS 14 deployment target. Nothing is compiled on your machine at install time.
How it is signed. With an ad-hoc signature (codesign --sign -) plus Hardened Runtime and an entitlements file. Hardened Runtime is not optional here: without it macOS will not show the Calendar or Reminders permission dialog when the binary runs as a subprocess of a GUI app such as Claude Desktop. The project is not signed with an Apple Developer ID and is not notarized.
Why macOS asks for permission again after an update. An ad-hoc signature has no stable identity, so macOS identifies the binary by the hash of its contents. Every release recompiles it, the hash changes, and TCC treats it as a program it has never seen. Grant Calendar and Reminders access again in System Settings → Privacy & Security when prompted. This is a consequence of not having a Developer ID; it is not a bug, and re-granting is safe.
If the prompt does not appear at all, doctor reports the Calendar and Reminders permission status and tells you which app to enable.
Servers
Uses a hybrid implementation: direct read-only SQLite queries for fast message reads and searches, plus JXA for actions like fetching bodies, listing attachments, and mutating message state.
Start with list_mail_accounts: it reports every account and mailbox with its unread count, which is where the values for the provider and mailbox filters come from. A filter that matches nothing returns an empty result rather than an error, so guessing is hard to tell apart from an empty mailbox.
Tools: list_mail_accounts, unread_emails, mark_emails_read, fetch_email_body, mark_emails_junk, mark_emails_not_junk, flag_emails, list_email_attachments, fetch_email_attachment, search_emails, extract_email_links, send_email, reply_email, forward_email
Contacts
CRUD for Apple Contacts people and groups via JXA.
Tools: contacts_people, contacts_groups
Notes
Full CRUD for Apple Notes folders and notes, plus search, via JXA.
Tools: list_folders, create_folder, list_notes, get_note, create_note, update_note, move_note, append_to_note, delete_note, delete_folder, search_notes
Memory
Structured memory store with subject-action-object triples, aliases, and duration queries.
Tools: create_entry, update_entry, get_entry, search_entries, query_last_occurrence, query_duration_since
Messages
Hybrid implementation: direct read-only SQLite queries against ~/Library/Messages/chat.db for reading and searching, plus JXA for sending messages. Supports both 1:1 and group chat sending. Inspired by @griches/apple-messages-mcp (MIT).
Tools: list_chats, get_messages, search_messages, get_participants, send_message
Events
Manages Apple Calendar events via a compiled Swift binary using EventKit. Supports full CRUD with structured locations, alarms, recurrence rules, and availability status.
Tools: calendar_events, calendar_calendars
Reminders
Manages Apple Reminders via EventKit. Supports full CRUD with subtasks (stored as checklists in notes), tags (native #tag format), priority levels, location triggers, alarms, and recurrence rules.
Tools: reminders_tasks, reminders_lists, reminders_subtasks
Examples
Everything below is typed into Claude Desktop as a normal chat message. Claude picks the tool, calls it, and answers from the result. Names, addresses and IDs are invented.
"Give me a summary of all unread emails"
Claude calls unread_emails on the apple_mail server with { "limit": 25 }. The tool returns grouped text (plus the same data as structuredContent):
Found 4 unread emails.
Northwind (work) — 3 unread
1. [2026-09-18T08:42:00] Priya Raman — Q3 pipeline review: agenda (INBOX)
2. [2026-09-18T07:15:00] GitHub — [northwind/api] PR #412 ready for review (INBOX)
3. [2026-09-17T18:03:00] Sam Okafor — Re: contractor invoice (INBOX)
Personal (personal) — 1 unread
4. [2026-09-18T06:58:00] Bainbridge Library — Hold ready for pickup (INBOX)Claude's reply, roughly:
You have 4 unread. Work (3): Priya sent the agenda for today's Q3 pipeline review; GitHub says PR #412 on northwind/api is ready for review; Sam followed up on the contractor invoice thread. Personal (1): the library has a hold ready for pickup. Want me to open any of these?
Follow-ups that work: "Read me Sam's email" (fetch_email_body), "Mark the GitHub one as read" (mark_emails_read), "Any links in the library email?" (extract_email_links).
"What reminders are due or overdue as of today, ranked by priority?"
Claude calls reminders_tasks on apple_reminders twice, { "action": "read", "dueWithin": "overdue" } and { "action": "read", "dueWithin": "today" }, then sorts. Each call returns markdown:
### Reminders (Total: 3)
- [ ] Renew car registration
- List: Errands
- ID: 7E1C2A4F-...
- Priority: high (1)
- Due: 2026-09-15
- [ ] Reply to school volunteer form
- List: Family
- ID: 0B93D6E2-...
- Priority: medium (5)
- Due: 2026-09-17
- [ ] Order printer toner
- List: Errands
- ID: C41F8B7D-...
- Due: 2026-09-16Claude's reply, roughly:
Overdue, highest priority first: 1) Renew car registration (high, due Sep 15, 3 days late). 2) Reply to school volunteer form (medium, due Sep 17). 3) Order printer toner (no priority, due Sep 16). Due today: Submit expense report (high), Call the vet (medium). Want me to mark any complete or push the toner one to Friday?
Follow-ups: "Mark the toner one done" (reminders_tasks with action: "update", id, completed: true), "Add 'pick up dry cleaning' to Errands for tomorrow, high priority" (action: "create").
"What's on my calendar tomorrow?"
Claude calls calendar_events on apple_events with { "action": "read", "startDate": "2026-09-19 00:00:00", "endDate": "2026-09-20 00:00:00" }:
### Calendar Events (Total: 2)
- Dentist
- Calendar: Family
- ID: 27985AF3-...:5C0E2D91-...
- Start: 2026-09-19T09:30:00-07:00
- End: 2026-09-19T10:30:00-07:00
- Location: 1200 Winslow Way E
- Team sync
- Calendar: Work
- ID: 5D22C354-...:a8f2k1@google.com
- Start: 2026-09-19T13:00:00-07:00
- End: 2026-09-19T13:30:00-07:00
- Availability: busyTwo things tomorrow: dentist at 9:30 on Winslow Way (an hour), and the 1:00 team sync for 30 minutes. The rest of the day is open.
If you leave the dates out, the read defaults to the next 14 days.
"Did anyone text me about Saturday's ride?"
Claude calls search_messages on apple_messages with { "query": "Saturday", "limit": 20 }:
Found 2 message(s) matching "Saturday":
[9/17/2026, 8:12:44 PM] [+14155550123] +14155550123: Still on for Saturday? Thinking 7am from the ferry.
[9/17/2026, 8:20:10 PM] [+14155550123] me: 7 works. Bring lights, it'll be dark.Yes. Dana asked last night if Saturday is still on and suggested 7am from the ferry; you replied that 7 works and to bring lights. Want me to add it to the Family calendar?
Follow-up: "Yes, 7 to 10, call it Group ride" (calendar_events with action: "create").
"Save a note with the three action items from today's review"
Claude calls create_note on apple_notes with { "folder": "Work", "title": "Q3 pipeline review — actions", "body": "<ul><li>Priya: send updated forecast by Fri</li><li>Me: review PR #412</li><li>Sam: resend invoice with PO number</li></ul>" }:
{
"success": true,
"name": "Q3 pipeline review — actions",
"folder": "Work"
}Saved to Notes under Work as "Q3 pipeline review — actions" with the three items.
"When did I last get the car serviced, and how long ago was that?"
The memory server is a small structured store for facts you want Claude to keep across conversations. Claude calls query_duration_since with { "subject": "car", "action": "serviced" }:
{
"source": "memory",
"query": { "subject": "car", "action": "serviced" },
"status": "matched",
"entry": {
"id": "b2c4e6f8-…",
"kind": "event",
"subject": "car",
"action": "serviced",
"happened_at": "2026-03-02"
},
"elapsed_days": 200
}Last service was March 2, about 200 days ago (6.5 months). If you're on a six-month interval, you're due.
To record one: "Remember that I serviced the car today" (create_entry).
Configuration
Data Directory
memory stores its SQLite database at:
MACOS_TOOLS_DATA_DIRif set~/.local/share/macos-tools/otherwise
Example:
MACOS_TOOLS_DATA_DIR=/path/to/data bunx @hachitogo/macos-mcp-tools memoryMail Account Classification
The mail server classifies each Apple Mail account — a label, a category and a provider — from email.json in the data directory above, so by default:
~/.local/share/macos-tools/email.jsonOn first use it scans your mailboxes and writes a starting point with every account labelled unknown. Edit it to give each account a real label and category; unread_emails groups and orders its output by them, and its warning names the full path.
A file it cannot parse is never overwritten. If you introduce a JSON error, mail reads fail with an error naming the file and the problem, rather than silently replacing your labels with defaults. Fix or delete the file and reads resume.
Upgrading from 0.7.1 or earlier: the config used to live inside the install directory, where every upgrade discarded it. An existing file is copied to the new location the first time a mail tool runs.
Testing
Lint and format check:
bun run lint(fix withbun run format)Unit tests:
bun testType checks:
bun run typecheckOpt-in integration tests:
bun run test:integrationOpt-in live macOS app smoke tests:
bun run test:integration:appsSwift binary build:
bun run build:swift(requires Xcode Command Line Tools). A git checkout must run this once beforebun test.
Integration tests are isolated and non-destructive. The default opt-in suite uses temporary data directories so it does not touch real memory databases. The live macOS app smoke tests are also read-only, but they do connect to your local Mail, Contacts, and Notes data and therefore remain separately opt-in.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Search, read, and write your Apple Notes from ChatGPT/Claude via a local Mac agent + MCP relay.
- mcpOAuthai.butlerbrain
Persistent memory for AI assistants. Save once; recall from Claude, ChatGPT, or any MCP client.
MCP connector for iMessage & Contacts via a local Mac agent + Vercel relay
Personal assistant MCP server with search, execute, packages, jobs, secrets, and integrations.
Related MCP Servers
- AlicenseBqualityCmaintenanceA macOS MCP server that provides AI agents with secure access to Calendar, Reminders, Notes, and Health data via native Apple APIs.3020MIT
- AlicenseBqualityBmaintenanceMCP server for macOS Apple apps. Enables read/write access to Notes, Reminders, Calendar, Contacts, and Safari using SQLite and JXA, all running locally.2810 npm4MIT
- AlicenseNot gradedqualityDmaintenanceA read-only MCP server that exposes data from native macOS apps (Mail, Notes, Calendar, Reminders, Contacts, Messages, Spotlight) to AI agents over stdio, currently in early development with no domain tools wired yet.3 npm4MIT
- AlicenseNot gradedqualityBmaintenanceA collection of small MCP servers for macOS automation that expose Apple Mail, MoneyMoney, and Spotlight functionality as tools via AppleScript and command-line, enabling AI applications to search mail, manage contacts and transactions, and perform system-wide file searches through natural language.MIT