@torsday/omnifocus-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,724 @@
1
+ # omnifocus-mcp
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@torsday/omnifocus-mcp.svg?label=npm)](https://www.npmjs.com/package/@torsday/omnifocus-mcp)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
5
+ [![Node: 24+](https://img.shields.io/badge/node-24%2B-brightgreen)](./package.json)
6
+ [![Platform: macOS 13+](https://img.shields.io/badge/platform-macOS%2013%2B-lightgrey)](https://www.apple.com/macos/)
7
+
8
+ > **Give any MCP-compatible AI assistant full, typed access to your OmniFocus.** Read your inbox, create tasks, close projects, batch-update dozens of items, evaluate perspectives, trigger sync — all through natural language. `omnifocus-mcp` wires an 80-tool MCP server directly to OmniFocus on macOS via JXA and OmniJS, with circuit breakers, rate limits, and an agent-aware error hierarchy so the assistant knows exactly what to do next when something goes wrong.
9
+
10
+ ---
11
+
12
+ ## Table of contents
13
+
14
+ - [Why this exists](#why-this-exists)
15
+ - [Quick start](#quick-start)
16
+ - [Example interactions](#example-interactions)
17
+ - [Prompts](#prompts)
18
+ - [If you are an AI agent](#if-you-are-an-ai-agent)
19
+ - [Tools](#tools)
20
+ - [Resources](#resources)
21
+ - [Transport text DSL](#transport-text-dsl)
22
+ - [Architecture at a glance](#architecture-at-a-glance)
23
+ - [Status and roadmap](#status-and-roadmap)
24
+ - [Install](#install)
25
+ - [Environment variables](#environment-variables)
26
+ - [Troubleshooting](#troubleshooting)
27
+ - [Client setup guides](#client-setup-guides)
28
+ - [Design documents](#design-documents)
29
+ - [Contributing](#contributing)
30
+ - [License](#license)
31
+
32
+ ---
33
+
34
+ ## Why this exists
35
+
36
+ OmniFocus is a powerful GTD tool, but it's an island. Your tasks sit there while you context-switch between your AI assistant and your task manager, manually copy-pasting notes, updating projects, and trying to keep everything in sync with your actual work.
37
+
38
+ `omnifocus-mcp` removes that friction. With it connected, your AI assistant can:
39
+
40
+ - **Capture** — turn a conversation into tasks directly in OmniFocus, with the right project, tags, due dates, and notes, without you touching the app
41
+ - **Review** — pull today's overdue items, this week's forecast, or a full project breakdown into context so the assistant can reason about your workload alongside your work
42
+ - **Maintain** — batch-defer a pile of overdue tasks, complete a sprint's worth of items, reorganize projects after a meeting debrief
43
+ - **Reflect** — ask "what's in my inbox right now?" or "what projects haven't been reviewed in a month?" and get structured, actionable answers
44
+
45
+ The server is built to a single-user local-first standard: no network surface, no cloud sync, typed errors with agent-readable remediation hints, safe by default.
46
+
47
+ ---
48
+
49
+ ## Quick start
50
+
51
+ 1. **Install**
52
+ ```bash
53
+ npm install -g @torsday/omnifocus-mcp
54
+ ```
55
+
56
+ 2. **Connect your MCP client** — the server speaks the standard MCP stdio protocol. For Claude Desktop, add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "omnifocus": {
61
+ "command": "omnifocus-mcp",
62
+ "args": [],
63
+ "env": { "OMNIFOCUS_LOG_LEVEL": "info" }
64
+ }
65
+ }
66
+ }
67
+ ```
68
+ Any MCP client that supports stdio transport (Claude Desktop, Claude Code, Cursor, Windsurf, etc.) uses the same `command` / `args` / `env` shape.
69
+
70
+ 3. **Grant macOS Automation permission** on first use — the app running the MCP server will prompt to control OmniFocus; click **OK**. If denied by mistake: **System Settings → Privacy & Security → Automation → [app] → OmniFocus** ✓
71
+
72
+ 4. **Verify** — ask your assistant: *"Use the internal_status tool and tell me what it returns."*
73
+
74
+ Detailed per-client guides: [`docs/clients/`](./docs/clients/)
75
+
76
+ ---
77
+
78
+ ## Example interactions
79
+
80
+ **"What's in my inbox right now?"**
81
+
82
+ The assistant calls `task_list` with `{ "available": true, "limit": 20 }` and returns a formatted list of actionable inbox tasks with their IDs, due dates, and flags.
83
+
84
+ ---
85
+
86
+ **"Create a task to 'review Q2 budget' due Friday, flagged, in the Finance project."**
87
+
88
+ 1. Calls `project_list` to find the Finance project ID.
89
+ 2. Calls `task_create` with `{ "name": "review Q2 budget", "projectId": "<id>", "dueDate": "end-of-week", "flagged": true }`.
90
+ 3. Returns the created task with its persistent ID and confirms the due date resolved to the correct Friday.
91
+
92
+ ---
93
+
94
+ **"Mark all my overdue tasks as deferred to tomorrow."**
95
+
96
+ 1. Calls `task_list` with `{ "dueBefore": "today", "available": true }` to find overdue items.
97
+ 2. Calls `task_batch_update` with `{ "deferDate": "tomorrow" }` for all of them in one atomic call.
98
+ 3. Reports: *"Deferred 7 overdue tasks to tomorrow. Call sync_trigger if you want iCloud to update immediately."*
99
+
100
+ ---
101
+
102
+ **"Show me what's due this week in the Work perspective."**
103
+
104
+ 1. Calls `perspective_list` to find the "Work" perspective ID.
105
+ 2. Calls `perspective_evaluate` with `{ "perspectiveId": "<id>" }` to get tasks in that perspective.
106
+ 3. Filters and presents items with due dates within the current week.
107
+
108
+ ---
109
+
110
+ **"I just finished the sprint — complete all tasks in the Mobile App project."**
111
+
112
+ 1. Calls `project_get` to retrieve the project and its tasks.
113
+ 2. Calls `task_batch_complete` with the full list of task IDs in one call.
114
+ 3. Confirms the count and suggests calling `sync_trigger` for cross-device visibility.
115
+
116
+ ---
117
+
118
+ ## Prompts
119
+
120
+ `omnifocus-mcp` ships four **MCP prompt templates** — structured workflows you can invoke by name from any MCP client that supports prompts (e.g. Claude Desktop's prompt picker, or any client that surfaces `prompts/list`).
121
+
122
+ ### `daily-review` — triage your day
123
+
124
+ Loads your snapshot, overdue tasks, and today's forecast; reschedules or drops overdue items; confirms due-today tasks; processes the inbox. No parameters needed.
125
+
126
+ ```
127
+ Use the daily-review prompt
128
+ ```
129
+
130
+ ### `weekly-review` — walk your projects
131
+
132
+ Loads every project whose review date has arrived; checks each one for stale tasks; marks it reviewed or completes/drops it. No parameters needed.
133
+
134
+ ```
135
+ Use the weekly-review prompt
136
+ ```
137
+
138
+ ### `capture-meeting` — extract action items
139
+
140
+ Takes raw meeting notes and creates OmniFocus tasks for every commitment, follow-up, and decision point. Pass the notes as text and optionally a project ID.
141
+
142
+ ```
143
+ Use the capture-meeting prompt with notes="Sync with Alice: she'll send the report by Thursday.
144
+ Bob to review the contract. Need to schedule follow-up call."
145
+ ```
146
+
147
+ Results in two inbox tasks: "Send report to [person]" and "Review contract" with the source sentences as notes.
148
+
149
+ ### `project-planning` — decompose a brief
150
+
151
+ Creates a new project and populates it with a set of concrete, ordered, one-day tasks derived from a free-text brief.
152
+
153
+ ```
154
+ Use the project-planning prompt with name="Q3 Marketing Site" brief="Redesign the marketing
155
+ site landing page and pricing page. New brand colors, updated copy, responsive mobile layout.
156
+ Launch by end of July."
157
+ ```
158
+
159
+ Results in a new OmniFocus project with 8–12 tasks covering design, copy, development, and review phases, ready to schedule and assign.
160
+
161
+ ---
162
+
163
+ ## If you are an AI agent
164
+
165
+ This section is written for you. It covers the conventions you need to use this MCP effectively without trial and error.
166
+
167
+ ### IDs, not names
168
+
169
+ Every OmniFocus resource — tasks, projects, tags, folders — is identified by a **persistent opaque ID** (e.g. `"hKx9vLmNp2"`). Names collide and change; IDs don't. Always resolve names to IDs with the corresponding `*_list` tool before calling any other tool.
170
+
171
+ ### Error codes and what to do next
172
+
173
+ Every error carries a stable `code`, a human-readable `suggestion`, and a machine-readable `remediationClass`:
174
+
175
+ | `remediationClass` | Meaning | Your action |
176
+ |--------------------|---------|-------------|
177
+ | `environment` | OmniFocus is not running, permissions denied, or a Pro/version feature is missing | Stop. Surface `suggestion` to the user; do not retry automatically |
178
+ | `input` | Bad ID, invalid field value, schema violation, or loop detected | Fix the input using `details` for specifics; retry |
179
+ | `transient` | Timeout, rate limit, queue full, or circuit open | Wait `details.retryAfterMs` ms, then retry once |
180
+ | `infrastructure` | JXA or OmniJS script failed | Retry once; if still failing, surface to user |
181
+ | `lifecycle` | Server is shutting down | Reconnect to a fresh server instance |
182
+
183
+ `RateLimited` and `CircuitOpen` always include `details.retryAfterMs` (default `60000` ms). Do not poll faster than that.
184
+
185
+ ### Dates
186
+
187
+ All date inputs accept either **ISO-8601 with UTC offset** (`"2026-04-22T09:00:00-07:00"`) or a **relative shortcut**:
188
+
189
+ `today` · `tomorrow` · `yesterday` · `this-week` · `next-week` · `end-of-week` · `end-of-month`
190
+
191
+ Shortcuts resolve to midnight in the server's local timezone.
192
+
193
+ ### Mutations and sync
194
+
195
+ Every write tool returns the full updated domain object, not just an acknowledgement. The response `meta.syncPending` is `true` immediately after a write — OmniFocus has saved locally but not yet synced to iCloud. Call `sync_trigger` if cross-device visibility matters; otherwise sync happens automatically within a few minutes.
196
+
197
+ ### Null consistency
198
+
199
+ All optional scalar fields are **always present** in responses, set to `null` when unset. You can safely destructure without null-checks on field presence.
200
+
201
+ ### Idempotency — safe retries
202
+
203
+ `project_create`, `project_update`, and `project_delete` accept an optional `idempotency_key?: string`. If you supply one and the call succeeds, replaying the exact same key within 5 minutes returns the cached result with `meta.idempotentReplay: true` and skips the OmniFocus call. Use a deterministic key scoped to your session and intent (e.g. `"session-abc/create-project-finance"`).
204
+
205
+ ### Dry-run — validate before committing
206
+
207
+ `task_update` and `project_update` accept `dry_run?: boolean`. When `true`, input is fully validated and the would-be result is returned, but nothing is written to OmniFocus. `meta.dryRun: true` is set on the response.
208
+
209
+ ### Additive tag edits — no read-modify-write needed
210
+
211
+ `task_update` accepts `addTags`, `removeTags`, and `setFlagged` patch fields alongside the existing full-replacement `tagIds` field. Prefer these for incremental edits — they apply a diff atomically inside the write queue with no race against concurrent user edits.
212
+
213
+ ### Conflict detection — optimistic concurrency
214
+
215
+ `task_update` accepts `expectedModifiedAt`. If the task was modified since your read, the server returns `OF_CONFLICT` (`remediationClass: "input"`). Re-read with `task_get`, merge your changes, and retry with the fresh `modifiedAt`.
216
+
217
+ ### Loop detection — don't get stuck
218
+
219
+ If you call the same tool with identical arguments 5+ times in a 60-second window, the server appends `WARN_LOOP_DETECTED` to `meta.warnings`. At 10 repetitions it throws `OF_LOOP_DETECTED` (`remediationClass: "input"`). Act on the result of your previous call rather than repeating it.
220
+
221
+ ### Capabilities pre-flight
222
+
223
+ Read `omnifocus://capabilities` once at session start. It returns OF version, edition (Standard/Pro), transport availability, and feature flags (`customPerspectives`, `forecastTag`, `rawScriptTools`). Use it to skip Pro-gated tools rather than discovering unavailability via error.
224
+
225
+ ### Rate limit state — self-throttle before hitting the wall
226
+
227
+ Every response includes `meta.rateLimit?: { remaining: number; resetAt: string }`. Check this after each call. If `remaining < 10`, slow down. If `remaining === 0`, do not call before `meta.rateLimit.resetAt`. The default limit is 120 calls/min per tool.
228
+
229
+ ### Structured warnings — act on `meta.warnings[].code`
230
+
231
+ Non-fatal issues appear in `meta.warnings` as `{ code, message, suggestion?, details? }`. Switch on `code`, not `message`:
232
+
233
+ | `code` | Means | Action |
234
+ |---|---|---|
235
+ | `WARN_IDS_NOT_FOUND` | Some IDs in a bulk call were not found | Check `details.missing` |
236
+ | `WARN_RESULT_TRUNCATED` | Response hit size limit; more items exist | Follow pagination cursor |
237
+ | `WARN_SYNC_PENDING` | Write saved locally; iCloud sync not yet triggered | Call `sync_trigger` if needed |
238
+ | `WARN_LOOP_DETECTED` | Same tool+args called ≥5 times in 60s | Act on previous result before repeating |
239
+
240
+ ### Incremental sync — `updatedSince`
241
+
242
+ `task_list` accepts `updatedSince?: string` (ISO-8601 or relative shortcut). Use it to fetch only changed items after your initial load:
243
+
244
+ ```jsonc
245
+ // First call: full load
246
+ { "available": true, "limit": 200 }
247
+
248
+ // Subsequent calls: only changes
249
+ { "available": true, "updatedSince": "2026-04-21T10:00:00-07:00", "limit": 200 }
250
+ ```
251
+
252
+ Note: deleted items cannot be surfaced via `updatedSince` — compare `meta.snapshot` counts if you need to detect deletions.
253
+
254
+ ### Navigation hints — follow `_links`
255
+
256
+ Every `Task` response includes `_links` with resource URIs for related objects:
257
+
258
+ ```jsonc
259
+ {
260
+ "id": "hKx9vLmNp2",
261
+ "_links": {
262
+ "self": "omnifocus://task/hKx9vLmNp2",
263
+ "project": "omnifocus://project/pXY3",
264
+ "tags": ["omnifocus://tag/tABC"]
265
+ }
266
+ }
267
+ ```
268
+
269
+ Pass the ID fragment to `task_get`, `project_get`, etc. You never need to construct a URI manually.
270
+
271
+ ### Response envelope
272
+
273
+ All responses have this shape:
274
+
275
+ ```jsonc
276
+ // success
277
+ { "data": { … }, "meta": { "correlationId": "…", "durationMs": 12, "cacheHit": false, "transport": "jxa", "syncPending": false } }
278
+
279
+ // error
280
+ { "error": { "code": "OF_NOT_FOUND", "remediationClass": "input", "message": "…", "suggestion": "…", "details": { … } }, "meta": { … } }
281
+ ```
282
+
283
+ ### Where to start
284
+
285
+ - **Daily work**: `task_list` (inbox or today filter) → `task_create` / `task_update` / `task_complete`
286
+ - **Projects**: `project_list` → `project_create` / `project_update`
287
+ - **Finding things**: `task_search` (keyword + optional tag/project/date filters); `tag_list` for available tags
288
+ - **Bulk ops**: `task_batch_create` / `task_batch_update` / `task_batch_complete` for up to 50 items atomically
289
+ - **Sync**: `sync_trigger` after bulk mutations; `internal_status` to check server health
290
+
291
+ ---
292
+
293
+ ## Tools
294
+
295
+ 80 tools are registered, organized by domain. See [`docs/tools.md`](./docs/tools.md) for the full auto-generated reference with input schemas, example calls, and example responses.
296
+
297
+ ### App lifecycle
298
+ | Tool | Description |
299
+ |---|---|
300
+ | `app_launch` | Explicitly launch OmniFocus (idempotent) |
301
+
302
+ ### Tasks
303
+ | Tool | Description |
304
+ |---|---|
305
+ | `task_list` | List tasks with filters (available, flagged, due, project, tag, updatedSince) |
306
+ | `task_get` | Get a single task by ID |
307
+ | `task_get_many` | Get multiple tasks by ID in one call |
308
+ | `task_create` | Create a task (project, tags, due, defer, flag, note, repeat) |
309
+ | `task_update` | Update a task (addTags/removeTags, dry_run, expectedModifiedAt) |
310
+ | `task_complete` | Mark a task complete |
311
+ | `task_uncomplete` | Unmark a completed task |
312
+ | `task_delete` | Delete a task |
313
+ | `task_drop` | Drop (defer indefinitely) a task |
314
+ | `task_undrop` | Restore a dropped task |
315
+ | `task_move` | Reparent a task to a different project or parent task |
316
+ | `task_reorder` | Reorder a task among its siblings |
317
+ | `task_duplicate` | Duplicate a task (optionally recursive) |
318
+ | `task_find_by_name` | Find tasks by exact or fuzzy name match |
319
+ | `task_search` | Full-text search across task names and notes, with optional tag/project/date/availability filters |
320
+ | `task_set_repetition` | Set a repeat rule on a task |
321
+ | `task_clear_repetition` | Remove a repeat rule from a task |
322
+ | `task_parse_transport_text` | Parse transport text DSL → structured tasks (no side effects) |
323
+ | `task_batch_create` | Create up to 50 tasks atomically |
324
+ | `task_batch_update` | Update up to 50 tasks atomically |
325
+ | `task_batch_complete` | Complete up to 50 tasks atomically |
326
+
327
+ ### Projects
328
+ | Tool | Description |
329
+ |---|---|
330
+ | `project_list` | List projects with filters (folder, status) |
331
+ | `project_get` | Get a single project by ID |
332
+ | `project_create` | Create a project (idempotency_key supported) |
333
+ | `project_update` | Update a project (dry_run, idempotency_key, expectedModifiedAt) |
334
+ | `project_complete` | Mark a project complete |
335
+ | `project_delete` | Delete a project (idempotency_key supported) |
336
+ | `project_drop` | Drop (defer indefinitely) a project |
337
+ | `project_move` | Move a project to a different folder |
338
+ | `project_mark_reviewed` | Mark a project as reviewed (alias for review_mark_reviewed) |
339
+
340
+ ### Folders
341
+ | Tool | Description |
342
+ |---|---|
343
+ | `folder_list` | List folders |
344
+ | `folder_get` | Get a single folder by ID |
345
+ | `folder_create` | Create a folder |
346
+ | `folder_update` | Rename a folder |
347
+ | `folder_delete` | Delete a folder |
348
+ | `folder_move` | Move a folder to a parent folder |
349
+
350
+ ### Tags
351
+ | Tool | Description |
352
+ |---|---|
353
+ | `tag_list` | List tags |
354
+ | `tag_get` | Get a single tag by ID |
355
+ | `tag_create` | Create a tag |
356
+ | `tag_update` | Rename a tag |
357
+ | `tag_delete` | Delete a tag |
358
+ | `tag_move` | Move a tag under a parent tag |
359
+ | `tag_set_status` | Set tag status (active/on-hold/dropped) |
360
+ | `tag_set_allows_next_action` | Toggle "allows next action" on a tag |
361
+ | `tag_get_location` | Get a tag's location in the hierarchy |
362
+ | `tag_set_location` | Set a tag's location in the hierarchy |
363
+
364
+ ### Notes
365
+ | Tool | Description |
366
+ |---|---|
367
+ | `note_get` | Get a task or project note (plain text) |
368
+ | `note_get_html` | Get a task or project note (HTML) |
369
+ | `note_set` | Set a task or project note (plain text, replaces) |
370
+ | `note_set_html` | Set a task or project note (HTML, replaces) |
371
+ | `note_append` | Append text to a task or project note |
372
+
373
+ ### Attachments
374
+ | Tool | Description |
375
+ |---|---|
376
+ | `attachment_list` | List attachments on a task or project |
377
+ | `attachment_add` | Embed a local file as an attachment |
378
+ | `attachment_remove` | Remove an attachment by ID |
379
+ | `attachment_save_to_path` | Save an attachment's bytes to a local file |
380
+
381
+ ### Perspectives
382
+ | Tool | Description |
383
+ |---|---|
384
+ | `perspective_list` | List all perspectives (built-in and custom) |
385
+ | `perspective_evaluate` | Evaluate a perspective and return its tasks |
386
+
387
+ ### Forecast & search
388
+ | Tool | Description |
389
+ |---|---|
390
+ | `forecast_get` | Get today's forecast grouped by overdue / due today / due later / inbox |
391
+ | `search_query` | Full-text search across tasks and projects |
392
+
393
+ ### Review
394
+ | Tool | Description |
395
+ |---|---|
396
+ | `review_list_due` | List projects whose next review date is today or past |
397
+ | `review_mark_reviewed` | Mark a project as reviewed and set the next review date |
398
+ | `review_set_interval` | Set the review interval (days) for a project |
399
+
400
+ ### Sync & app
401
+ | Tool | Description |
402
+ |---|---|
403
+ | `sync_trigger` | Trigger an OmniFocus iCloud sync |
404
+ | `sync_status` | Get the last sync timestamp and status |
405
+
406
+ ### Plug-ins
407
+ | Tool | Description |
408
+ |---|---|
409
+ | `plugin_invoke` | Invoke an installed Omni Automation plug-in by bundle identifier |
410
+
411
+ ### Export & import
412
+ | Tool | Description |
413
+ |---|---|
414
+ | `export_opml` | Export a project (or all projects) as OPML |
415
+ | `export_taskpaper` | Export a project (or all projects) as TaskPaper |
416
+ | `import_opml` | Import tasks from an OPML string into OmniFocus |
417
+ | `import_taskpaper` | Import tasks from a TaskPaper string into OmniFocus |
418
+
419
+ ### Observability
420
+ | Tool | Description |
421
+ |---|---|
422
+ | `internal_status` | Server health: transport status, queue depths, cache stats, rate limits |
423
+
424
+ ### Raw scripts _(opt-in, off by default)_
425
+ | Tool | Description |
426
+ |---|---|
427
+ | `run_jxa_script` | Execute arbitrary JXA — requires `OMNIFOCUS_ALLOW_RAW_SCRIPT=1` |
428
+ | `run_omnijs_script` | Execute arbitrary OmniJS — requires `OMNIFOCUS_ALLOW_RAW_SCRIPT=1` |
429
+
430
+ ---
431
+
432
+ ## Resources
433
+
434
+ Ten MCP resources are registered under the `omnifocus://` scheme. Resources are read-only, URI-addressable, and enumerable via `resources/list`.
435
+
436
+ | URI | Returns |
437
+ |---|---|
438
+ | `omnifocus://capabilities` | Server capabilities: OF version, edition, transport status, feature flags |
439
+ | `omnifocus://snapshot` | Five-count orientation object: inbox, flagged, overdue, dueToday, projectsDueForReview |
440
+ | `omnifocus://inbox` | Inbox tasks as `Task[]` |
441
+ | `omnifocus://forecast/today` | Today's forecast grouped by overdue / due today / due later / inbox |
442
+ | `omnifocus://overdue` | All overdue tasks sorted by dueDate ASC |
443
+ | `omnifocus://flagged` | All flagged available tasks |
444
+ | `omnifocus://review-due` | Projects with nextReviewDate ≤ today |
445
+ | `omnifocus://project/{id}` | Single project + full task tree |
446
+ | `omnifocus://tag/{id}` | Single tag + its tasks |
447
+ | `omnifocus://perspective/{id}` | Perspective evaluation result (same shape as `perspective_evaluate`) |
448
+
449
+ ---
450
+
451
+ ## Transport text DSL
452
+
453
+ `task_parse_transport_text` parses a lightweight DSL inspired by OmniFocus Mail Drop into structured task objects. **No tasks are created** — pass the returned `tasks[]` to `task_create` or `task_batch_create` separately.
454
+
455
+ ### Token syntax
456
+
457
+ | Token | Example | Meaning |
458
+ |---|---|---|
459
+ | `@tag` | `@work` | Assign a tag by name |
460
+ | `#date` | `#2026-05-01` or `#today` | Due date |
461
+ | `::date` | `::tomorrow` | Defer date |
462
+ | `!!` | `!!` | Flag the task |
463
+ | `//text` | `//Call back before noon` | Append as task note |
464
+ | `Project: Name` | `Project: Finance` | Set project context for subsequent tasks |
465
+
466
+ ### Date shortcuts
467
+
468
+ `today` · `tomorrow` · `yesterday` — resolved to midnight local time.
469
+
470
+ Full ISO-8601 dates (`YYYY-MM-DD`) are also accepted. Unparseable dates emit a `warnings[]` entry.
471
+
472
+ ### Example
473
+
474
+ ```
475
+ Project: Work
476
+ Prepare Q2 report @work #end-of-week !!
477
+ Send draft to Alice @work @email #tomorrow //attach spreadsheet
478
+ Follow up with Bob ::next-week
479
+
480
+ Project: Personal
481
+ Buy groceries @errands #today
482
+ Call dentist @phone ::tomorrow !! //ask about X-ray appointment
483
+ ```
484
+
485
+ Tag names and project names are raw strings — resolve to IDs with `tag_list` and `project_list` before passing to `task_create`.
486
+
487
+ ---
488
+
489
+ ## Architecture at a glance
490
+
491
+ ```mermaid
492
+ flowchart LR
493
+ Agent["LLM agent<br/>(any MCP client)"] --> SDK["MCP stdio<br/>transport"]
494
+ SDK --> Tools["Tool &<br/>Resource handlers"]
495
+ Tools --> Services["Service layer"]
496
+ Services --> Cache[(30s LRU<br/>read cache)]
497
+ Cache --> Adapter{OmniFocus<br/>Adapter}
498
+ Adapter --> Router[Transport<br/>Router]
499
+ Router -->|CRUD, forecast, search| Jxa[JxaTransport]
500
+ Router -->|Perspectives, plug-ins,<br/>reorder, reparent| OmniJs[OmniJsTransport]
501
+ Jxa --> OF[(OmniFocus)]
502
+ OmniJs --> OF
503
+
504
+ classDef boundary stroke-dasharray: 5 5
505
+ class Adapter boundary
506
+ ```
507
+
508
+ **Key design points:**
509
+
510
+ - **Adapter seam** — services never see `osascript` or URL schemes; `OmniFocusAdapter` is the only OS boundary. Tests swap in an `InMemoryAdapter`.
511
+ - **Dual transport** — JXA via `osascript` for CRUD; OmniJS via `evaluateJavascript()` for custom perspectives, plug-ins, reorder, and reparent. A `TransportRouter` picks per operation.
512
+ - **Read pool + write queue** — concurrent JXA reads from a configurable pool; mutations serialized through a write queue; OmniJS operations through a separate queue.
513
+ - **30s LRU read cache** — invalidated on every write. Mutations are never served stale.
514
+ - **Middleware stack** — every registered tool runs through: `assertNotShuttingDown` → `circuitBreaker` → `rateLimitMeta` → `loopDetection`.
515
+
516
+ The full layered diagram with queues, circuit breakers, and the test adapter lives in [`DESIGN.md §6`](./DESIGN.md#6-architecture).
517
+
518
+ ---
519
+
520
+ ## Status and roadmap
521
+
522
+ All six milestones shipped. v1.0.0 is in preparation for npm release — see the [unreleased section of the CHANGELOG](./CHANGELOG.md#unreleased) for what's queued.
523
+
524
+ | Phase | Milestone | Status |
525
+ |---|---|---|
526
+ | M0 | Foundation + both transports | ✅ Done |
527
+ | M1 | Core task & project surface | ✅ Done |
528
+ | M2 | Metadata + perspectives (OmniJS) | ✅ Done |
529
+ | M3 | Advanced (repeat, notes, review, batch, DSL) | ✅ Done |
530
+ | M4 | Long tail (attachments, OPML, sync, plug-ins, raw scripts) | ✅ Done |
531
+ | M5 | Polish & release (observability, E2E, CI, docs, npm) | ✅ Done |
532
+
533
+ Track open issues and future enhancements on the [**GitHub Project board**](https://github.com/users/torsday/projects/4).
534
+
535
+ ---
536
+
537
+ ## Install
538
+
539
+ ```bash
540
+ # Global install
541
+ npm install -g @torsday/omnifocus-mcp
542
+
543
+ # Or run without installing (npx)
544
+ npx -y @torsday/omnifocus-mcp
545
+ ```
546
+
547
+ **Any stdio MCP client** — add to your client's MCP server config (exact key name varies by client):
548
+ ```json
549
+ {
550
+ "mcpServers": {
551
+ "omnifocus": {
552
+ "command": "omnifocus-mcp",
553
+ "args": [],
554
+ "env": { "OMNIFOCUS_LOG_LEVEL": "info" }
555
+ }
556
+ }
557
+ }
558
+ ```
559
+
560
+ **npx (no global install)**:
561
+ ```json
562
+ {
563
+ "mcpServers": {
564
+ "omnifocus": {
565
+ "command": "npx",
566
+ "args": ["-y", "@torsday/omnifocus-mcp"],
567
+ "env": { "OMNIFOCUS_LOG_LEVEL": "info" }
568
+ }
569
+ }
570
+ }
571
+ ```
572
+
573
+ On first run, macOS asks the app running the server for permission to automate OmniFocus. Click **OK**. If you denied it by mistake: **System Settings → Privacy & Security → Automation → [app] → OmniFocus** ✓
574
+
575
+ See the [troubleshooting guide](./docs/troubleshooting.md) and per-client guides in [`docs/clients/`](./docs/clients/) for detailed setup.
576
+
577
+ ---
578
+
579
+ ## Environment variables
580
+
581
+ | Variable | What | Default |
582
+ |---|---|---|
583
+ | `OMNIFOCUS_LOG_LEVEL` | `trace`\|`debug`\|`info`\|`warn`\|`error` — logs go to stderr | `info` |
584
+ | `OMNIFOCUS_CACHE_TTL_MS` | Read-cache TTL in milliseconds | `30000` |
585
+ | `OMNIFOCUS_READ_POOL_SIZE` | Concurrent `osascript` processes for reads | `2` |
586
+ | `OMNIFOCUS_WRITE_QUEUE_CAP` | Max pending writes before `QueueFull` error | `50` |
587
+ | `OMNIFOCUS_JXA_TIMEOUT_MS` | Per-call JXA hard timeout in milliseconds | `30000` |
588
+ | `OMNIFOCUS_OMNIJS_TIMEOUT_MS` | Per-call OmniJS hard timeout in milliseconds | `45000` |
589
+ | `OMNIFOCUS_ATTACHMENT_PATHS` | Colon-separated allowlist of absolute path prefixes for attachment ops | `$HOME` |
590
+ | `OMNIFOCUS_MAX_ATTACHMENT_MB` | Maximum attachment file size in MB (0 = no cap) | `100` |
591
+ | `OMNIFOCUS_TOOL_RATE_LIMIT` | Per-tool rate limit in `N/SECONDS` format | `120/60` |
592
+ | `OMNIFOCUS_ALLOW_RAW_SCRIPT` | Set to `1` to register `run_jxa_script` / `run_omnijs_script` | unset |
593
+ | `OMNIFOCUS_INTEGRATION` | Set to `1` to enable the integration test suite | unset |
594
+
595
+ Full table with override semantics: [`DESIGN.md §22`](./DESIGN.md#22-configuration--environment).
596
+
597
+ ### Running integration tests
598
+
599
+ Integration tests run against a live OmniFocus install:
600
+
601
+ ```bash
602
+ # 1. Make sure OmniFocus is running and Automation permission is granted
603
+ # 2. Seed fixture data (idempotent — safe to re-run):
604
+ node scripts/seed-integration-db.js
605
+
606
+ # Optional: wipe and re-create all fixtures from scratch:
607
+ node scripts/seed-integration-db.js --clean
608
+
609
+ # 3. Run the integration suite:
610
+ OMNIFOCUS_INTEGRATION=1 pnpm test:integration
611
+ ```
612
+
613
+ The seed script creates `mcp-fixture:` prefixed items (folders, projects, tasks, tags) that integration tests rely on.
614
+
615
+ ---
616
+
617
+ ## Troubleshooting
618
+
619
+ ### OmniFocus is not running
620
+
621
+ **Error:** `OF_NOT_RUNNING` — OmniFocus must be open for most operations.
622
+
623
+ **Fix:** Launch OmniFocus manually, or call `app_launch` to open it via MCP.
624
+
625
+ ---
626
+
627
+ ### macOS Automation permission denied
628
+
629
+ **Symptom:** Every tool call returns `OF_PERMISSION_DENIED`.
630
+
631
+ **Fix:**
632
+ 1. Open **System Settings → Privacy & Security → Automation**.
633
+ 2. Find the app running the MCP server (Terminal, your AI client app, or your CI runner's shell).
634
+ 3. Enable the **OmniFocus** checkbox.
635
+ 4. Restart omnifocus-mcp.
636
+
637
+ ```bash
638
+ bash scripts/check-automation-permission.sh
639
+ ```
640
+
641
+ ---
642
+
643
+ ### First-call timeout / slow startup
644
+
645
+ JXA starts an `osascript` subprocess on each call. The first call after a system sleep or a fresh OmniFocus launch can take 5–15 seconds while the database loads. This is normal.
646
+
647
+ If calls consistently time out:
648
+ ```bash
649
+ OMNIFOCUS_JXA_TIMEOUT_MS=60000 omnifocus-mcp
650
+ ```
651
+
652
+ ---
653
+
654
+ ### `run_jxa_script` / `run_omnijs_script` not available
655
+
656
+ **Error:** `ValidationError: run_jxa_script is not available in this adapter configuration`
657
+
658
+ **Fix:** The raw-script tools are opt-in. Start the server with:
659
+ ```bash
660
+ OMNIFOCUS_ALLOW_RAW_SCRIPT=1 omnifocus-mcp
661
+ ```
662
+
663
+ See [`docs/adr/0004-raw-script-escape-hatch.md`](./docs/adr/0004-raw-script-escape-hatch.md) for the security rationale.
664
+
665
+ ---
666
+
667
+ ### Stale data after a write
668
+
669
+ Writes are saved locally and show up immediately in subsequent tool calls. Changes don't reach other devices until iCloud sync runs. Call `sync_trigger` after bulk mutations or when cross-device visibility matters.
670
+
671
+ ---
672
+
673
+ ### More help
674
+
675
+ - [`docs/troubleshooting.md`](./docs/troubleshooting.md) — expanded troubleshooting guide
676
+ - [`docs/clients/`](./docs/clients/) — per-client setup guides
677
+
678
+ ---
679
+
680
+ ## Client setup guides
681
+
682
+ | Client | Guide |
683
+ |---|---|
684
+ | Claude Desktop | [`docs/clients/claude-desktop.md`](./docs/clients/claude-desktop.md) |
685
+ | Claude Code (CLI) | [`docs/clients/claude-code.md`](./docs/clients/claude-code.md) |
686
+ | Generic stdio client | [`docs/clients/generic-stdio.md`](./docs/clients/generic-stdio.md) |
687
+
688
+ ---
689
+
690
+ ## Design documents
691
+
692
+ - **[`SPEC.md`](./SPEC.md)** — functional scope and non-functional requirements; resolved v1 decisions
693
+ - **[`DESIGN.md`](./DESIGN.md)** — 28-section architecture; options evaluated; R/S/M assessment; example tool implementation
694
+ - **[`docs/security.md`](./docs/security.md)** — attack surface, mitigations, and test coverage
695
+ - **[`docs/domain-reference.md`](./docs/domain-reference.md)** — OmniFocus glossary, canonical schemas, lossiness matrix for export/import
696
+ - **[`docs/adr/`](./docs/adr/)** — Architecture Decision Records covering every load-bearing choice:
697
+
698
+ | # | Decision |
699
+ |---|---|
700
+ | [0001](./docs/adr/0001-language-and-runtime.md) | TypeScript on Node.js 24 |
701
+ | [0002](./docs/adr/0002-omnifocus-transport-dual.md) | JXA + OmniJS dual transport |
702
+ | [0003](./docs/adr/0003-tool-surface-namespaced.md) | `<noun>_<verb>` tool namespacing |
703
+ | [0004](./docs/adr/0004-raw-script-escape-hatch.md) | Opt-in raw-script tools |
704
+ | [0005](./docs/adr/0005-script-assets-as-files.md) | Scripts as first-class files |
705
+ | [0006](./docs/adr/0006-read-cache-strategy.md) | 30s LRU, invalidate-on-write |
706
+ | [0007](./docs/adr/0007-dates-iso8601-with-offset.md) | ISO-8601 with offset at the boundary |
707
+ | [0008](./docs/adr/0008-ids-branded-opaque-strings.md) | Branded opaque ID types |
708
+ | [0009](./docs/adr/0009-concurrency-pool-and-queue.md) | Read pool + write queue + OmniJS queue |
709
+ | [0010](./docs/adr/0010-mcp-transport-stdio.md) | stdio-only MCP transport (v1) |
710
+ | [0011](./docs/adr/0011-versioning-and-stability.md) | Semver with explicit contract |
711
+ | [0012](./docs/adr/0012-distribution-npx.md) | Distribution via `npx` / npm |
712
+ | [0013](./docs/adr/0013-tool-response-envelope.md) | Uniform response envelope |
713
+
714
+ ---
715
+
716
+ ## Contributing
717
+
718
+ This is a single-developer project; external contributions are not currently solicited. The design, ADRs, and task backlog are public so the work is inspectable and forkable. See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the patterns any contribution would need to follow.
719
+
720
+ ---
721
+
722
+ ## License
723
+
724
+ [MIT](./LICENSE) — see full text in `LICENSE`.