omnifocus-mcp
CLAUDE.md8 starsChanged 8 months agoMITView on GitHub
An MCP server bridging Claude and OmniFocus via AppleScript on macOS. Stack: Python 3.10+, FastMCP, AppleScript (via osascript) Version: v0.13.2 | Tests: 1027 unit, 197 integration/E2E/protocol, 47 benchmark/profiling | Coverage: 93% Running the server: uv run python -m omnifocus_mcp.server_fastmcp or via Claude Desktop config. The API was consolidated from 40+ functions to 16 in October 2025. v0.12.0 added unified batch CRUD with Pydantic models. v0.13.0 removed deprecated single-item wrappers. Resist adding new functions. Projects (5): create_projects, get_projects, update_projects, delete_projects, reorder_project…
In this repository5 other files it gives its agents.
- api-designSkill
- applescript-omnifocusSkill
- integration-testingSkill
- performance-patternsSkill
- releaseSkill
The file
We split this CLAUDE.md into the rules it gives an agent
5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 46
Variable naming conflicts: Never use variable names that match OmniFocus properties.
- Very specific: line 50
Recurring task completion: Use mark complete (AppleScript command), NOT set completed to true (property).
- Very specific: line 56
String escaping: Always use _escape_applescript_string() for user-provided text.
- Distinctive: line 73
Default timeout: 60s, max: 300s (configurable).
- Distinctive: line 95
Hard rule: If you wrote or modified an AppleScript string in the connector, integration tests must cover that operation before merge.
≈ “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
OmniFocus MCP Server
An MCP server bridging Claude and OmniFocus via AppleScript on macOS.
Stack: Python 3.10+, FastMCP, AppleScript (via osascript)
Version: v0.13.2 | Tests: 1027 unit, 197 integration/E2E/protocol, 47 benchmark/profiling | Coverage: 93%
Commands
make test # Unit tests (~2min, mocked AppleScript)
make test-integration # Real OmniFocus tests (~30s, requires test DB)
make test-e2e # End-to-end MCP tool tests (requires test DB)
make test-prod # Production DB tests (OmniAutomation, sandbox folder)
./scripts/check_complexity.sh # Cyclomatic complexity check
./scripts/check_client_server_parity.sh # Verify all client functions are exposed in server
./scripts/check_version_sync.sh # Version consistency across files
./scripts/check_dependencies.sh # Dependency vulnerability scan (pip-audit)
./scripts/check_applescript_safety.sh # AppleScript unsafe pattern detectionRunning the server: uv run python -m omnifocus_mcp.server_fastmcp or via Claude Desktop config.
API Surface (21 MCP tools)
The API was consolidated from 40+ functions to 16 in October 2025. v0.12.0 added unified batch CRUD with Pydantic models. v0.13.0 removed deprecated single-item wrappers. Resist adding new functions.
Projects (5): create_projects, get_projects, update_projects, delete_projects, reorder_project Tasks (5): create_tasks, get_tasks, update_tasks, delete_tasks, reorder_task Folders (3): create_folders, get_folders, update_folders Tags (4): create_tags, get_tags, update_tags, delete_tags Perspectives (2): get_perspectives, switch_perspective Navigation (2): set_focus, get_focus
Core API Principles
- Comprehensive update functions over specialized operations — no
set_due_date(), useupdate_task(task_id, due_date=X) - No field-specific setters or getters —
update_taskhandles all fields,get_taskshandles all filters - Separate single/batch updates — batch excludes name/note (require unique values)
- Union types for deletes only —
Union[str, list[str]]for delete operations, NOT for updates - No upsert pattern — create and update are always separate
- Structured returns — always
dictorlist[dict], never formatted text strings
AppleScript Gotchas (these cause real bugs)
Variable naming conflicts: Never use variable names that match OmniFocus properties. Using name, note, status, flagged etc. as AppleScript variable names creates silent conflicts where OmniFocus reads the property instead. Always prefix: taskName, taskNote, projectStatus.
Rich text notes: OmniFocus stores notes as rich text internally. Reading note of task returns plain text, but setting note of task replaces rich text with plain text. There is no workaround — document this limitation.
Recurring task completion: Use mark complete (AppleScript command), NOT set completed to true (property). The command creates the next occurrence; the property just marks the current one done without spawning the next recurrence.
JSON helpers duplicated 9x: AppleScript has no imports/modules. Every AppleScript block that returns JSON must include its own helper functions. This is intentional.
Date format: AppleScript uses "March 5, 2026 5:00:00 PM" format. The connector handles ISO 8601 conversion via _iso_to_applescript_date().
String escaping: Always use _escape_applescript_string() for user-provided text. Unescaped quotes break AppleScript blocks silently.
Performance Constraints
The bottleneck is per-property IPC cost. Each AppleScript property read from OmniFocus costs ~17ms (inter-process communication). With 26 properties per task, iterating 381 tasks = ~168s. This single fact explains nearly all performance issues. See docs/reference/PERFORMANCE_PROFILING.md for full experiment data.
whose clauses are 20-30x faster than manual iteration. OmniFocus evaluates whose natively:
flattened tasks whose flagged is true: 0.22s- Manual loop checking
flagged of t: 6.59s - Current
get_tasks(flagged)(loop + 26 props): 18.9s
What is NOT a bottleneck: Loop iteration itself (0.17s for 381 tasks), string concatenation (<100ms for 400 items), JSON building.
Current baselines (35 projects, ~202 tasks, 10 tags, 4 folders — see profiling doc for full table):
get_tasks(flagged): 0.66s (14) |get_tasks(inbox): 0.64s (10) |get_tasks(query): 1.07s (37)get_tasks()(all): 2.20s (202) |get_projects(): 0.57s (35) | Write ops: 0.9s
Default timeout: 60s, max: 300s (configurable).
Architecture: All get_tasks() paths use batch mode (a reference to) for O(P) property reads. whose clauses further pre-filter when available (20-30x faster than without).
Project task health: get_projects(include_task_health=True) returns per-project task counts in a single AppleScript call. +133% overhead due to nested per-project task loops.
Conditional data loading: get_projects() supports include_last_activity=True for expensive per-project calculations (+54% overhead).
Database Safety
Destructive operations require OMNIFOCUS_TEST_MODE=true and OMNIFOCUS_TEST_DATABASE=OmniFocus-TEST.ofocus environment variables. Each destructive operation verifies the database name via AppleScript before proceeding. This prevents accidental production data loss during development.
Testing Requirements
| Type | When Required | How |
| Unit tests | Every code change | make test (pre-push hook) |
| Integration tests | New/modified AppleScript operations | make test-integration (requires test DB) |
| Prod integration tests | OmniAutomation / evaluate javascript changes | make test-prod (sandbox folder in prod DB) |
| Performance benchmarks | Changes to fetch/filter paths | tests/benchmarks/ |
| Blind agent evals | Before release if tool descriptions changed | Issue #227 |
Hard rule: If you wrote or modified an AppleScript string in the connector, integration tests must cover that operation before merge. Unit tests mock run_applescript() and cannot catch AppleScript syntax errors, variable naming collisions, or OmniFocus behavioral quirks.
Branch Convention
{type}/issue-{num}-{description} — e.g., feature/issue-42-batch-tags, fix/issue-99-timeout
CHANGELOG.md is only updated on release branches, never on feature branches.
Skills
Load these skills when working in their domains:
- release — Full release workflow: milestone check, version bump, changelog, validation, tagging, PR
- applescript-omnifocus — OmniFocus AppleScript patterns, quirks, and workarounds
- api-design — API consolidation philosophy, decision tree for new functions, anti-patterns
- performance-patterns — Batch fetching, round-trip minimization, filter architecture
- integration-testing — Real-OmniFocus testing, test database setup, why mocks miss bugs
Key Files
src/omnifocus_mcp/omnifocus_connector.py— Core AppleScript client (~3350 lines)src/omnifocus_mcp/server_fastmcp.py— FastMCP server wrapping the connector (~1050 lines)docs/reference/ARCHITECTURE.md— Full API design rationaledocs/reference/APPLESCRIPT_GOTCHAS.md— Complete AppleScript limitations reference
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.

