macos-mcp
CLAUDE.md8 starsChanged 6 months agoMITView on GitHub
Local MCP server bridging Claude to six macOS apps (Reminders, Calendar, Notes, Mail, Messages, Contacts). The user's data is the truth: if Mail.app shows 7 messages, this server returns 7. Any discrepancy with the native app is a bug. This repo is publish-on-merge. .github/workflows/ci.yml runs semantic-release on every push to main, which publishes to npm as mcp-macos and cuts a GitHub release. That is a public, outward-facing act and it is not cleanly reversible — npm restricts unpublish after 72…
- Installs packages
In this repositoryOne other file it gives its agents.
- macosSkill
The file
We split this CLAUDE.md into the rules it gives an agent
11 rules. About 0 are common, 4 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 29
A PR held for release timing is left as a DRAFT on purpose: Do not mark it ready to clear a queue.
- Distinctive: line 60
Notes folders: No rename/delete via JXA (Apple API limitation)
- Distinctive: line 61
Messages: No delete/edit via JXA or SQLite (Apple API limitation)
- Very specific: line 64
Mail reads: Use SQLite (~/Library/Mail/V10/MailData/Envelope Index), JXA for writes only.
- Very specific: line 66
JXA rule: Always use whose() predicates for search, never JS iteration over collections
- Very specific: line 110
Zod validation: All inputs validated via schemas in validation/schemas.ts
- Very specific: line 111
Error handling: Use handleAsyncOperation() from errorHandling.ts.
- Very specific: line 112
JXA safety: Always use sanitizeForJxa() before interpolating user input
- Very specific: line 122
Safety: All enrichment paths protected by withTimeout(5000ms)
- Very specific: line 141
Before any release, run pnpm release:preview and verify the version bump matches intent.
- Distinctive: line 157
If a subsystem has no external callers AND Kyle doesn't remember what it does, default to cut, not preserve.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
Local MCP server bridging Claude to six macOS apps (Reminders, Calendar, Notes, Mail, Messages, Contacts). The user's data is the truth: if Mail.app shows 7 messages, this server returns 7. Any discrepancy with the native app is a bug.
⛔ MERGING TO main PUBLISHES A PUBLIC NPM PACKAGE
This repo is publish-on-merge. .github/workflows/ci.yml runs semantic-release
on every push to main, which publishes to npm as mcp-macos and cuts a GitHub
release. That is a public, outward-facing act and it is not cleanly reversible — npm
restricts unpublish after 72 hours.
So merging a PR here is not the same class of action as merging a PR in any other repo on this machine. It is a release.
fix:/feat:/perf:onmain→ a real npm publish.docs:/chore:/style:/refactor:/test:onmain→ CI runs, nothing publishes.- ⚠️ Exception: a
BREAKING CHANGE:footer publishes a MAJOR from any commit type,docs:included, and arevert:publishes a patch. Verified at source in the installed@semantic-release/commit-analyzer@13.0.1lib/default-release-rules.js— that file is the authority, not this list. pnpm release:previewdry-runs it and prints the next version before you commit to anything.
Rules for any agent, and especially for an unattended one:
- Never merge a
fix:/feat:/perf:PR here without Kyle saying so in that session. A general instruction to "get everything back on main" or "clean up the branches" is NOT that authorization — those sweeps are written for repos where a merge is recoverable, and here it is a publish. - A PR held for release timing is left as a DRAFT on purpose. Do not mark it ready
to clear a queue. A deliberate hold is marked on four surfaces — draft flag, a
[HELD — DO NOT MERGE: ...]title prefix, a blockquoted notice as the first block of the body, and ado-not-merge:publishes-npmlabel. The title prefix matters most: a sweep's inventory pass reads titles, not bodies. Set all four when you park one. - Doc-only changes are free — commit them normally.
⚠️ Do not "solve" this by deleting or renaming the local clone. ~/.local/bin/EventKitCLI
is a symlink into bin/ of this working copy, so removing it silently breaks every
Reminders and Calendar call on this machine — a failure that has already recurred twice
(2026-07-21, 2026-08-19). The guard belongs on the PR and in this file, not in the
filesystem.
Commands
pnpm install # Install dependencies
pnpm build # Build TypeScript + Swift binary (required before running)
pnpm test # Run all tests
pnpm lint # Lint and format with Biome
pnpm dev # Run from source via tsx (stdio only, no build needed)
pnpm test:e2e # Build + run functional E2E tests (node:test, real OS calls)
pnpm test:e2e:all # Build + run ALL per-tool E2E suites (serial, no JXA contention)
pnpm release:preview # Dry-run semantic-release (shows next version + changelog)bin/run.cjs runs compiled dist/index.js — requires pnpm build. Use pnpm dev for source-level iteration (stdio only).
Known Gotchas
- Calendar: Recurring event deletion only removes single occurrence (
.thisEventspan) - Notes folders: No rename/delete via JXA (Apple API limitation)
- Messages: No delete/edit via JXA or SQLite (Apple API limitation)
- Messages reads: JXA broken on Sonoma+ — all reads use SQLite at
~/Library/Messages/chat.db - Mail create: Creates draft only — user must click Send in Mail.app
- Mail reads: Use SQLite (
~/Library/Mail/V10/MailData/Envelope Index), JXA for writes only. See ADR-001 - Gmail labels: Messages live in
[Gmail]/All Mail. Folder membership is in thelabelsjoin table. BothlistInboxMessages()andlistMailboxMessages()check this. See ADR-001 addendum - JXA rule: Always use
whose()predicates for search, never JS iteration over collections - Notes title: Apple re-derives
n.name()from body on everyn.body =— update/append scripts re-set name after body assignment - EventKit date range:
predicateForEventscannot span >4 years. BothfindEventByIdandfindEventsdefault to ±2 years - Contact enrichment at scale: Per-handle JXA lookups don't scale beyond ~10 participants
Architecture
Three bridges to Apple apps:
- EventKit (Swift binary) — Reminders, Calendar
- JXA — Notes, Mail writes, Contacts, Messages send
- SQLite — Messages reads (
~/Library/Messages/chat.db), Mail reads (~/Library/Mail/V10/MailData/Envelope Index)
Key Files
src/
├── config/ # Zod schema + loadConfig() (auto-injects name/version from package.json)
├── server/
│ └── server.ts # MCP server factory
├── tools/
│ ├── definitions.ts # MCP tool schemas (dependentSchemas for validation)
│ ├── index.ts # Tool routing
│ └── handlers/ # Domain handlers (reminderHandlers.ts, notesHandlers.ts, etc.)
├── utils/
│ ├── cliExecutor.ts # Swift binary execution + permission retry
│ ├── contactResolver.ts # Cross-tool contact enrichment (name ↔ phone/email)
│ ├── jxaExecutor.ts # JXA/AppleScript execution + retry logic
│ ├── logging.ts # Structured error logging (tool failures → stderr)
│ ├── sqliteContactReader.ts # Contact enrichment cache via AddressBook SQLite (ADR-002)
│ ├── sqliteMailReader.ts # Mail SQLite reader (ADR-001)
│ ├── sqliteMessageReader.ts # Messages SQLite reader
│ └── preflight.ts # Startup --check validation
└── validation/
└── schemas.ts # Zod schemasData Flow
- EventKit: Handler → Repository →
executeCli()→ Swift binary → JSON - JXA: Handler →
buildScript()→executeJxaWithRetry()→ osascript → JSON
Key Patterns
- Zod validation: All inputs validated via schemas in
validation/schemas.ts - Error handling: Use
handleAsyncOperation()fromerrorHandling.ts. Permission errors include System Settings deep-link URLs. - JXA safety: Always use
sanitizeForJxa()before interpolating user input - Date formats:
YYYY-MM-DD HH:mm:ssfor local time, ISO 8601 for UTC
Contact Enrichment
Cross-tool layer resolves phone numbers and emails to contact names (Messages, Mail, Calendar).
- Handle→name: SQLite bulk cache from AddressBook DB (<50ms for 1100+ entries). See ADR-002
- Name→handles: Targeted JXA
whose()search — O(log n) - Startup:
warmCache()called fire-and-forget, prevents cold cache timeouts - Safety: All enrichment paths protected by
withTimeout(5000ms) - Toggle:
enrichContactsparam (default: true)
Testing
- Unit: Jest with ts-jest ESM. Coverage thresholds in
jest.config.mjs(95%/80%/95%/95%). If coverage drops, write tests for uncovered branches. - E2E:
pnpm test:e2e— node:test suite, real MCP client, real OS calls. Tests use[E2E-TEST]prefix for cleanup. - Mocks:
src/utils/__mocks__/cliExecutor.ts,src/tools/jxaHandlers.test.ts
Development Workflow
This is a public repository.
- Features and fixes → branch + PR. Issue first, link it (
Closes #XX) - Chores, docs, config → direct to
main. No PR ceremony for trivial changes - Pre-push hook (
scripts/hooks/pre-push) runs tests + build automatically
Releasing
Before any release, run pnpm release:preview and verify the version bump matches intent. fix: → patch, feat: → minor, BREAKING CHANGE: → major.
Distribution
- npm: Published as
mcp-macos - CI: GitHub Actions — test + lint + release (#86)
Competitive Landscape
mattt/iMCP(1418 stars) is the long-term threat — Swift.app+ menu-bar UX, covers Messages/Contacts/Calendar/Reminders but not Mail or Notes. Different product category (ships as.app, not npm). If iMCP adds Mail+Notes, the wedge closes.supermemoryai/apple-mcp(3087 stars, JXA-based) was archived January 2026. Still pulls 1.1k npm dl/mo on inertia.- Differentiators to preserve: SQLite reads for Mail (
Envelope Index) and Messages on Sonoma+, cross-tool contact enrichment via SQLite AddressBook cache. Nobody else combines these. - Posture: quietly available. No registry submission, no
awesome-mcp-serversPR, no announcement.
Scope Discipline
- If a subsystem has no external callers AND Kyle doesn't remember what it does, default to cut, not preserve. Two no's = dead weight regardless of LOC.
- Post-pivot, deletion is highest-leverage work. INTENT.md framing changed (phone-from-coffee-shop → local-only on Mac Mini); the right response was cutting HTTP transport + opinionated prompts subsystem, not maintaining them.
Commits
Use /commit skill.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

