@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/CHANGELOG.md +114 -0
- package/LICENSE +21 -0
- package/README.md +724 -0
- package/dist/index.js +4804 -0
- package/package.json +86 -0
package/README.md
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
# omnifocus-mcp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@torsday/omnifocus-mcp)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](./package.json)
|
|
6
|
+
[](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`.
|