Skip to main content
Glama
README.md
# omnifocus-sync-mcp

A headless [MCP](https://modelcontextprotocol.io) server that reads **and writes**
OmniFocus data by talking **directly to the Omni Sync Server** over WebDAV — no Mac,
no running OmniFocus app. It implements OmniFocus's client-side end-to-end encryption
in TypeScript, so it can run anywhere Node runs (a Linux NAS, a container, a cloud box).

Every other OmniFocus MCP server automates the local macOS app via AppleScript/JXA.
This one speaks the cloud sync protocol instead, which is what lets it run headless.

> ⚠️ **Alpha / use at your own risk.** This talks to your real task database. The
> read and write paths have been validated end-to-end against a live Omni Sync
> Server account (add + delete transactions round-trip correctly), and the crypto
> and parsing have an offline test suite — but this hasn't been exercised across
> every OmniFocus version or edge case. **Keep a backup** (in OmniFocus: File → Back
> Up Database), and consider a throwaway account while evaluating. See [Safety](#safety).

## How it works

An OmniFocus sync store is a WebDAV collection (`OmniFocus.ofocus/`) containing:

- a baseline transaction and a chain of delta transactions, each a `.zip` holding a
  `contents.xml` describing object creates/updates/deletes;
- `.client` files registering each device and the transaction "tail" it has synced to;
- an `encrypted` plist holding the wrapped document keys.

This server:

1. **Resolves the shard** — `sync.omnigroup.com` 302-redirects each account to a
   `syncN.omnigroup.com` host; the client follows it.
2. **Authenticates** with HTTP Digest (what Omni Sync Server negotiates).
3. **Decrypts** using the OmniFileStore format: PBKDF2 derives a wrapping key from your
   passphrase, RFC 3394 unwraps the document key slots, and each file is AES-128-CTR +
   HMAC-SHA256 in 64 KiB segments (encrypt-then-MAC).
4. **Replays** the transaction chain into an in-memory object model.
5. **Writes** by building a new `contents.xml`, zipping, encrypting, `PUT`-ting it as a
   new transaction descending from the current head tail, and refreshing its `.client`
   registration so other clients treat it as a real participant.

The crypto and format were reimplemented from Omni's **own open-source frameworks**
([OmniGroup/OmniGroup](https://github.com/omnigroup/OmniGroup), notably
`OmniFileStore/EncryptionFormat.md` and `DecryptionExample.py`) plus community
reverse-engineering ([tomzx/ofocus-format](https://github.com/tomzx/ofocus-format),
[rubyfocus](https://github.com/jyruzicka/rubyfocus)). See [CREDITS](#credits).

## Install

```bash
git clone https://github.com/rosskukulinski/omnifocus-sync-mcp.git
cd omnifocus-sync-mcp
npm install
npm run build
```

## Configure

Set credentials via environment variables (see [`.env.example`](./.env.example)):

| Variable | Required | Description |
| --- | --- | --- |
| `OMNIFOCUS_SYNC_USERNAME` | yes | Omni Sync Server account name |
| `OMNIFOCUS_SYNC_PASSWORD` | yes | Omni Sync Server account password |
| `OMNIFOCUS_ENCRYPTION_PASSPHRASE` | no | Defaults to the sync password (OmniFocus's default) |
| `OMNIFOCUS_SYNC_URL` | no | Defaults to `https://sync.omnigroup.com` |
| `OMNIFOCUS_DATABASE` | no | Defaults to `OmniFocus.ofocus` |
| `OMNIFOCUS_CLIENT_NAME` | no | Display name for this client's `.client` file |
| `OMNIFOCUS_CLIENT_STATE_FILE` | no | Where the stable client id is stored |
| `OMNIFOCUS_READ_ONLY` | no | `1`/`true` to refuse all writes |

## Try it without an MCP client

A diagnostic CLI exercises the sync path directly:

```bash
export OMNIFOCUS_SYNC_USERNAME=... OMNIFOCUS_SYNC_PASSWORD=...
npm run probe -- sync            # show database counts
npm run probe -- list flagged    # list flagged tasks
npm run probe -- projects        # list projects
npm run probe -- add "Buy milk"  # add an inbox task
```

## Use as an MCP server

Run over stdio. Example Claude Desktop / Claude Code config:

```json
{
  "mcpServers": {
    "omnifocus": {
      "command": "node",
      "args": ["/absolute/path/to/omnifocus-sync-mcp/dist/index.js"],
      "env": {
        "OMNIFOCUS_SYNC_USERNAME": "your-account",
        "OMNIFOCUS_SYNC_PASSWORD": "your-password"
      }
    }
  }
}
```

### Tools

| Tool | Description |
| --- | --- |
| `of_sync` | Reload the database from the server; returns counts |
| `of_list_tasks` | List tasks (`remaining`, `available`, `flagged`, `inbox`, `due_soon`, `all`), with project/tag/search filters |
| `of_get_task` | Full detail for one task by id |
| `of_list_projects` | List projects with folder path and status |
| `of_add_task` | Create a task (inbox or in a project) |
| `of_complete_task` | Mark a task complete |
| `of_edit_task` | Edit name/note/flag/due/defer; clear dates |
| `of_delete_task` | Delete a task by id |

## Safety

- **Back up first**, and prefer a **test account** until you trust the write path.
- Start with `OMNIFOCUS_READ_ONLY=1` to explore safely.
- Writes are append-only transactions; a bad write forks history rather than
  destroying data, and OmniFocus merges forks on its next sync — but that is a
  safety net, not a guarantee.
- Secrets come from the environment only; nothing is written to this repo. The client
  identity file (`client.json`) contains no secrets.

## Development

```bash
npm test          # offline test suite (crypto round-trips, parse/write, digest)
npm run dev -- ... # run the CLI via tsx without building
```

The offline tests cover the risky parts without a network or a real account: the
OmniFileStore encrypt/decrypt round-trip at every segment boundary, MAC tamper
detection, the passphrase→PBKDF2→key-unwrap pipeline, `contents.xml` build/parse
round-trips, partial-update merge semantics, and the full zip→encrypt→decrypt→parse
read path.

## Credits

Built by reimplementing formats documented and open-sourced by others:

- [OmniGroup/OmniGroup](https://github.com/omnigroup/OmniGroup) — Omni's own frameworks;
  `OmniFileStore/EncryptionFormat.md` and `DecryptionExample.py` are the authoritative
  encryption spec, and `OmniFoundation/XML/OFXMLIdentifier.m` the id format.
- [tomzx/ofocus-format](https://github.com/tomzx/ofocus-format) — the `.ofocus`
  transaction/format write-up.
- [jyruzicka/rubyfocus](https://github.com/jyruzicka/rubyfocus),
  [kumpelblase2/focus](https://github.com/kumpelblase2/focus) — prior-art parsers.

Not affiliated with or endorsed by The Omni Group. "OmniFocus" is their trademark.

## License

MIT — see [LICENSE](./LICENSE).

TDQS

A3.9/5.0

Scored across 8 tools

Disambiguation5/5

Each tool targets a distinct operation: sync, list/get tasks, list projects, and task CRUD (add, complete, edit, delete). No two tools overlap in purpose, making selection unambiguous.

Naming Consistency4/5

Most tools follow the verb_noun pattern (list_tasks, get_task, add_task, etc.) with a consistent 'of_' prefix. 'of_sync' is a minor deviation, being a single verb without an explicit object, but the pattern is otherwise uniform.

Tool Count5/5

8 tools is well within the ideal range for a focused sync server, covering core task and project operations without over-fragmentation. Each tool earns its place.

Completeness4/5

The tool surface covers the main task lifecycle (create, read, update, complete, delete) and project listing, which is adequate for most workflows. Minor gaps include no project creation/editing or dedicated tag listing, but these are not critical dead ends.

Maintenance

ActivityStale
ResponsivenessNo issues