Skip to main content
Glama
estrenuo

OmniFocus MCP Server

by estrenuo

OmniFocus MCP Server

A Model Context Protocol (MCP) server that enables AI assistants to interact with OmniFocus on macOS via JXA (JavaScript for Automation).

Features

This MCP server provides access to OmniFocus functionality:

Task Management

  • List inbox tasks - View and filter tasks in your inbox (including multi-tag filtering)

  • Create tasks - Add new tasks with full property support (due dates, planned dates, tags, notes, subtasks, recurrence)

  • Update tasks - Change name, note, dates, flag, estimate, recurrence, or move a task to another project

  • Complete/Drop tasks - Mark tasks as done or dropped, individually or in batch

  • Delete tasks - Remove a task permanently

  • Update task notes - Replace, clear, or append to a task's note

  • Get due tasks - Find tasks due within a timeframe

  • Get planned tasks - Find tasks planned within a timeframe

  • Get flagged tasks - List all flagged items

  • Add/remove tags from tasks - Manage task tags, individually or in batch

Project Management

  • List projects - View projects with status filtering

  • Get project tasks - List all tasks belonging to a project

  • Create projects - New projects with folder placement, status, dates, sequential mode, review interval

  • Update projects - Change name, note, status, flag, dates, sequential mode, review interval

  • Delete projects - Remove a project and its tasks

  • Update project notes - Replace, clear, or append to a project's note

  • Get projects for review - Find projects needing review, optionally with their incomplete tasks

  • Mark project reviewed - Update a project's review status and next review date

  • Batch mark reviewed - Efficiently review multiple projects at once

Organization

  • List folders - View folder hierarchy

  • Create/rename/delete folders - Manage the folder tree (including nested folders)

  • List tags - View all tags

  • List perspectives - View built-in and custom perspectives

  • Get perspective tasks - List tasks shown in a specific perspective

  • Universal search - Search across tasks, projects, folders, and tags

Safety properties

  • No silent winner on duplicate names. OmniFocus allows two projects (or tasks) to share a name. Name lookups collect every match and fail with the matching IDs when there is more than one, so a rename, move or delete can never hit the wrong item while reporting success.

  • Mutations are verified. Operations that JXA can fail silently at (notably moving a task between projects) read the result back inside the same script, so a failed move is reported as an error rather than as success.

Related MCP server: OmniFocus MCP Server

Requirements

  • macOS (OmniFocus is macOS/iOS only, and this server uses JXA)

  • OmniFocus 3+ installed

  • Node.js 18+

  • Automation permissions enabled for your terminal/client app

Installation

  1. Clone or download this repository:

    cd omnifocus-mcp-server
  2. Install dependencies:

    npm install
  3. Build the TypeScript:

    npm run build
  4. Configure your MCP client to use the server (see Configuration below)

Configuration

Claude Desktop

Add to your Claude Desktop configuration file (~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "omnifocus": {
      "command": "/opt/homebrew/bin/node",
      "args": ["/path/to/omnifocus-mcp-server/dist/index.js"]
    }
  }
}

Use an absolute path to the node binary. A bare "node" resolves against the GUI session's PATH, which does not include Homebrew or a version manager's shims, so the server fails to start with a "server unreachable"-style error even though it runs fine from your terminal. Find yours with which node.

Other MCP Clients

The server uses stdio transport by default, so configure your client to spawn:

node /path/to/omnifocus-mcp-server/dist/index.js

Remote access (HTTP transport)

For remote clients — most importantly claude.ai custom connectors, which is how the Claude iOS app reaches MCP servers — the server can run as a Streamable HTTP endpoint:

MCP_TRANSPORT=http \
MCP_AUTH_TOKEN="$(openssl rand -hex 32)" \
node /path/to/omnifocus-mcp-server/dist/index.js

Environment variables:

Variable

Default

Purpose

MCP_TRANSPORT

stdio

Set to http to enable the HTTP transport

MCP_HTTP_PORT

3000

Port to listen on

MCP_HTTP_HOST

127.0.0.1

Bind address (keep loopback; expose via a tunnel)

MCP_AUTH_TOKEN

Required shared secret; the server refuses to start without it

MCP_PUBLIC_URL

Public HTTPS origin the server is reachable at (e.g. https://your-tunnel-host). Set this to enable the OAuth layer needed by claude.ai / Claude Desktop's "Connectors" UI — see below. Leave unset for direct/programmatic clients, which only need the static token.

OMNIFOCUS_SCRIPT_TIMEOUT_MS

60000

Kill a JXA script that hangs (applies to both transports)

The MCP endpoint is /mcp. Authentication accepts either an Authorization: Bearer <token> header, or the token as a path segment (/mcp/<token>) for clients that cannot send custom headers. GET /health is unauthenticated.

Reaching it from claude.ai / the iOS app. Custom connectors connect from Anthropic's cloud (not from your device), so the endpoint must be publicly reachable over HTTPS. A Cloudflare Tunnel or a Tailscale Funnel both work for this — either way, a local process makes an outbound connection, so no ports are opened. (Plain Tailscale, without Funnel, does not: that only reaches your own tailnet, and Anthropic's cloud isn't on it.) Optionally restrict access to Anthropic's outbound IP range (160.79.104.0/21) in a Cloudflare WAF rule.

claude.ai's and Claude Desktop's "Connectors" UI (as opposed to a raw MCP config / direct API client) always drives a full OAuth handshake before it will call a remote server — it does not accept the static token by itself, even embedded in the URL. Set MCP_PUBLIC_URL to your tunnel's public origin to turn on a self-issued OAuth layer that satisfies this while still gating access with the same static secret (see oauth.ts / CLAUDE.md for how). With that set, add the connector in Settings → Connectors using the path-token URL (https://your-tunnel-host/mcp/<token>) — the "Connect" step will complete the OAuth handshake automatically. If your tunnel also proxies / to a different local service, make sure /authorize, /token, /register, and /.well-known/* are mapped to this server too, or the OAuth requests never arrive here.

The Mac must stay awake with OmniFocus running (caffeinate -s or Amphetamine).

Session semantics — single client only. The HTTP transport serves one session at a time: a new initialize replaces the previous session. Every tool call is stateless, and a spec-compliant client re-initializes when it receives 404 for a session that no longer exists, so a single client calling sequentially works fine. That 404 also covers the case where the server holds no session at all, which is what happens to every client after the server restarts — they re-initialize instead of treating it as a dead endpoint.

Two clients at once do not. Reproduced by firing two concurrent initializetools/list sequences at the public endpoint: one consistently 404s. The underlying MCP SDK binds a single transport to the shared server instance, so the losing session is evicted and its in-flight request either 404s or hangs. Fixing this requires a per-session McpServer instance rather than the current registration-on-a-singleton pattern; it is not currently planned. See CLAUDE.md for the details.

Troubleshooting a "can't connect" client. Clients collapse every remote failure into one vague message, so read this server's log (StandardErrorPath of your launch agent) rather than the client's wording — the status code says which of three unrelated things went wrong:

What the log shows

Meaning

Fix

↳ path token rejected: got N chars …

The token in the client's URL is wrong or truncated

Re-copy <MCP_PUBLIC_URL>/mcp/<token> whole; never retype it

↳ authorize rejected: resource … !== …

Same cause, seen from the OAuth side. A /authorize → 302 with no /token after it always means this

As above

[…] → 404 then a fresh initialize

Normal recovery after a restart or a session takeover

Nothing; the client re-initializes itself

[initialize] → 200 — client: …

Working. The client name identifies which one

If nothing appears in the log at all, the request never reached this server: check the tunnel's path mappings, not this code.

Permissions

On first use, macOS will prompt you to allow automation access:

  1. Go to System PreferencesSecurity & PrivacyPrivacyAutomation

  2. Enable permission for your terminal or Claude Desktop to control OmniFocus

Tool Reference

All 31 tools are listed below, grouped by area.

omnifocus_list_inbox

List tasks in the inbox, optionally filtered by tags.

{
  "includeCompleted": false,
  "limit": 50,
  "tags": ["Work", "Urgent"],
  "tagMatchMode": "all"
}

tagMatchMode is "all" (task has every listed tag, default), "any" (at least one), or "none" (none of them). It only applies when tags is given. The same two parameters work on omnifocus_get_due_tasks, omnifocus_get_flagged_tasks and omnifocus_get_planned_tasks.

omnifocus_list_projects

List projects with filtering.

{
  "status": "active",
  "folderName": "Work",
  "limit": 50
}

omnifocus_get_project_tasks

Get all tasks belonging to one project.

{
  "projectId": "abc123",
  "includeCompleted": false,
  "limit": 100
}

omnifocus_create_project

Create a project, optionally inside a folder.

{
  "name": "Website redesign",
  "note": "Q1 initiative",
  "folderName": "Work",
  "dueDate": "2024-03-31T17:00:00",
  "deferDate": "2024-01-15T09:00:00",
  "flagged": false,
  "sequential": false,
  "status": "active",
  "reviewIntervalDays": 7
}

status is "active" (default), "on hold", "done" or "dropped". sequential: false (default) makes a parallel project.

omnifocus_update_project

Update project properties. Identify by projectId or projectName (ID wins).

{
  "projectId": "abc123",
  "name": "Website redesign v2",
  "status": "on hold",
  "flagged": true,
  "dueDate": null,
  "sequential": true,
  "reviewIntervalDays": 14
}

Pass null for note, dueDate or deferDate to clear them. A project cannot be moved to another folder (JXA limitation).

omnifocus_delete_project

Delete a project and its tasks. Identify by projectId or projectName (ID wins).

{
  "projectId": "abc123"
}

omnifocus_list_folders

List all folders.

{
  "status": "active",
  "limit": 50
}

omnifocus_create_folder

Create a folder, top-level or nested.

{
  "name": "Clients",
  "parentFolderName": "Work"
}

omnifocus_update_folder

Rename a folder. Identify by folderId or folderName (ID wins). A folder cannot be moved into another folder (JXA limitation).

{
  "folderName": "Clients",
  "name": "Key clients"
}

omnifocus_delete_folder

Delete a folder and everything in it. Identify by folderId or folderName (ID wins).

{
  "folderId": "abc123"
}

omnifocus_list_tags

List all tags.

{
  "status": "active",
  "limit": 50
}

omnifocus_list_perspectives

List perspectives (built-in and custom).

{
  "limit": 50
}

omnifocus_get_perspective_tasks

Get tasks shown in a specific perspective.

{
  "perspectiveName": "Next",
  "limit": 50
}

omnifocus_create_task

Create a new task.

{
  "name": "Review quarterly report",
  "note": "Check all sections",
  "projectName": "Work",
  "dueDate": "2024-12-31T17:00:00",
  "deferDate": "2024-12-01T09:00:00",
  "plannedDate": "2024-12-15T09:00:00",
  "flagged": true,
  "estimatedMinutes": 60,
  "tagNames": ["Review", "Important"],
  "parentTaskId": "xyz789",
  "recurrence": {
    "frequency": "weekly",
    "interval": 1,
    "daysOfWeek": ["Monday", "Thursday"],
    "repeatFrom": "due-date"
  }
}

Planned Date vs Due Date:

  • dueDate: When the task must be completed (deadline)

  • plannedDate: When you intend to work on the task (planning)

  • This distinction is crucial for separating deadlines from scheduled work time

Recurrence: frequency is "daily", "weekly", "monthly" or "yearly". Use daysOfWeek for weekly, dayOfMonth (1-31) for monthly, monthOfYear (1-12) for yearly. repeatFrom is "due-date" (default) or "completion-date".

Subtasks: pass parentTaskId to create the task as a child of an existing task.

omnifocus_update_task

Update an existing task. Identify by taskId or taskName (ID wins).

{
  "taskId": "abc123",
  "name": "Review quarterly report (final)",
  "note": null,
  "dueDate": "2024-12-20T17:00:00",
  "flagged": true,
  "estimatedMinutes": 45,
  "projectName": "Work"
}
  • Pass null for note, dueDate, deferDate or plannedDate to clear them; estimatedMinutes: 0 clears the estimate.

  • projectId / projectName moves the task to that project (subtasks come along). The move is verified afterwards, so a failure is reported as an error instead of a false success.

  • recurrence takes the same object as create_task; recurrence: null or clearRecurrence: true turns repetition off.

omnifocus_delete_task

Delete a task. Identify by taskId or taskName (ID wins).

{
  "taskId": "abc123"
}

omnifocus_update_task_note

Replace, clear, or append to a task's note. Identify by taskId or taskName (ID wins).

{
  "taskId": "abc123",
  "note": "Added after the call.",
  "append": true
}

An empty note string clears the note.

omnifocus_complete_task

Mark a task as complete or dropped. You can identify the task by either ID or name.

{
  "taskId": "abc123",
  "action": "complete"
}

Or using task name:

{
  "taskName": "Write documentation",
  "action": "complete"
}

Action can be "complete" (default) or "drop". If both taskId and taskName are provided, taskId takes priority.

Dropping a repeating task clears its repetition rule first, so the series really stops instead of rolling forward to the next occurrence.

omnifocus_batch_complete_task

Complete or drop up to 100 tasks by ID in one call.

{
  "taskIds": ["id1", "id2", "id3"],
  "action": "complete"
}

omnifocus_add_tag_to_task

Add a tag to a task. You can identify the task by either ID or name.

{
  "taskId": "abc123",
  "tagName": "Urgent"
}

Or using task name:

{
  "taskName": "Write report",
  "tagName": "Urgent"
}

If both taskId and taskName are provided, taskId takes priority.

omnifocus_remove_tag_from_task

Remove a tag from a task. You can identify the task by either ID or name.

{
  "taskId": "abc123",
  "tagName": "Urgent"
}

Or using task name:

{
  "taskName": "Old task",
  "tagName": "Done"
}

If both taskId and taskName are provided, taskId takes priority.

omnifocus_batch_add_tag

Add one existing tag to up to 100 tasks by ID.

{
  "taskIds": ["id1", "id2", "id3"],
  "tagName": "Urgent"
}

omnifocus_batch_remove_tag

Remove one tag from up to 100 tasks by ID.

{
  "taskIds": ["id1", "id2", "id3"],
  "tagName": "Urgent"
}

omnifocus_update_project_note

Replace, clear, or append to a project's note. Identify by projectId or projectName (ID wins).

{
  "projectName": "Website redesign",
  "note": "Kickoff moved to March.",
  "append": false
}

Search across OmniFocus.

{
  "query": "report",
  "searchType": "all",
  "limit": 20
}

omnifocus_get_due_tasks

Get tasks due within a timeframe.

{
  "daysAhead": 7,
  "includeOverdue": true,
  "limit": 50
}

omnifocus_get_flagged_tasks

Get flagged tasks.

{
  "includeCompleted": false,
  "limit": 50
}

omnifocus_get_planned_tasks

Get tasks planned within a timeframe.

{
  "daysAhead": 7,
  "includeOverdue": true,
  "limit": 50
}

omnifocus_get_projects_for_review

Get projects that need review based on their next review date. Perfect for GTD practitioners following the review workflow.

{
  "daysAhead": 0,
  "status": "active",
  "limit": 50,
  "includeTasks": true,
  "taskLimit": 50
}

Parameters:

  • daysAhead: How many days ahead to look (0 = overdue reviews only)

  • status: Filter by project status ("active", "done", "dropped", "onHold", "all")

  • limit: Maximum number of projects to return (1-500)

  • includeTasks: Include each project's incomplete tasks in the result (default false) — this turns one review pass into a single call instead of one follow-up call per project

  • taskLimit: Maximum tasks per project when includeTasks is true (1-200, default 50)

Each project also returns reviewInterval and lastReviewDate.

omnifocus_mark_project_reviewed

Mark a project as reviewed and update its next review date. You can identify the project by either ID or name.

{
  "projectId": "abc123"
}

Or using project name:

{
  "projectName": "Weekly Review"
}

With custom review interval:

{
  "projectName": "Work Project",
  "reviewIntervalDays": 14
}

Parameters:

  • projectId or projectName: Identifies the project (ID takes priority)

  • reviewIntervalDays (optional): Custom review interval in days. If not provided, uses the project's existing review interval.

omnifocus_batch_mark_reviewed

Mark multiple projects as reviewed in one efficient operation.

{
  "projectIds": ["id1", "id2", "id3"]
}

With custom review interval for all:

{
  "projectIds": ["id1", "id2", "id3"],
  "reviewIntervalDays": 7
}

Parameters:

  • projectIds: Array of project IDs to mark as reviewed (1-100 projects)

  • reviewIntervalDays (optional): Custom review interval to apply to all projects

Returns a summary with:

  • Count of successful reviews

  • Count of failures

  • Full project data for successful reviews

  • Error details for any failures

Date Formats

All dates use ISO 8601 format: YYYY-MM-DDTHH:mm:ss

Examples:

  • 2024-12-31T17:00:00 - December 31, 2024 at 5:00 PM

  • 2024-06-15T09:00:00 - June 15, 2024 at 9:00 AM

Error Handling

The server provides clear error messages for common issues:

  • OmniFocus not running: Launch OmniFocus first

  • Permission denied: Enable automation permissions in System Preferences

  • Item not found: The specified ID doesn't exist

  • Invalid parameters: Check parameter format and values

Development

Build

npm run build

Watch mode

npm run dev

Tests

npm test              # All unit tests
npm run test:watch    # Watch mode
npm run test:coverage # Coverage report (thresholds enforced: 80% lines, 75% branches)

The integration tests in src/__tests__/integration.test.ts are skipped by default: they require a running OmniFocus and they modify your real database.

Test manually

After building, you can test with:

echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node dist/index.js

Applying changes to a running server (important)

MCP clients fetch the tool list once when they connect and cache it for the session. npm run build alone does not update a client that is already connected — Node does not hot-reload, and the client will not re-fetch the schema. After changing tools/schemas you must both restart the server and make each client reconnect:

  1. Rebuild: npm run build

  2. Restart the server process so it loads the new dist/:

    • LaunchAgent (HTTP transport): launchctl kickstart -k gui/$(id -u)/com.sanderrobijns.omnifocus-mcp

    • Verify it serves the new schema: lsof -nP -iTCP:3000 -sTCP:LISTEN should show a freshly started PID.

  3. Reconnect each client so it re-fetches tools/list:

    • Claude Code / Cowork: start a new session (a running session keeps its cached schema for its whole lifetime).

    • Claude Desktop: quit and reopen the app (or toggle the server off/on).

    • claude.ai / Claude iOS custom connector: re-sync the connector in Settings → Connectors (it caches the tool list at the connector level).

Until the client reconnects it keeps showing the old schema, even though the server already serves the new one.

License

MIT

Credits

Built using:

Install Server
A
license - permissive license
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity
Issues opened vs closed

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…

  • Personal assistant MCP server with search, execute, packages, jobs, secrets, and integrations.

  • MCP connector that lets ChatGPT list, search, and run your Apple Shortcuts via a local Mac agent

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/estrenuo/omnifocus-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server