omnifocus-mcp-enhanced 1.8.0 → 1.10.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.
Files changed (62) hide show
  1. package/README.md +119 -1
  2. package/dist/context/omnifocusData.js +60 -0
  3. package/dist/context/prompts.js +156 -0
  4. package/dist/context/resources.js +57 -0
  5. package/dist/server.js +33 -1
  6. package/dist/tools/definitions/addTag.js +34 -0
  7. package/dist/tools/definitions/addTaskNotification.js +35 -0
  8. package/dist/tools/definitions/appendToNote.js +41 -0
  9. package/dist/tools/definitions/countTasks.js +41 -0
  10. package/dist/tools/definitions/duplicateTask.js +42 -0
  11. package/dist/tools/definitions/editTag.js +40 -0
  12. package/dist/tools/definitions/listTaskNotifications.js +47 -0
  13. package/dist/tools/definitions/removeTag.js +59 -0
  14. package/dist/tools/definitions/removeTaskNotification.js +35 -0
  15. package/dist/tools/definitions/searchTags.js +20 -0
  16. package/dist/tools/primitives/addTag.js +77 -0
  17. package/dist/tools/primitives/appendToNote.js +122 -0
  18. package/dist/tools/primitives/appendToNote.test.js +45 -0
  19. package/dist/tools/primitives/countTasks.js +34 -0
  20. package/dist/tools/primitives/duplicateTask.js +27 -0
  21. package/dist/tools/primitives/duplicateTask.test.js +85 -0
  22. package/dist/tools/primitives/editTag.js +194 -0
  23. package/dist/tools/primitives/listProjects.test.js +67 -0
  24. package/dist/tools/primitives/removeTag.js +97 -0
  25. package/dist/tools/primitives/searchTags.js +30 -0
  26. package/dist/tools/primitives/tagCrud.test.js +47 -0
  27. package/dist/tools/primitives/taskNotifications.js +78 -0
  28. package/dist/tools/primitives/taskNotifications.test.js +105 -0
  29. package/dist/utils/omnifocusScripts/duplicateTask.js +76 -0
  30. package/dist/utils/omnifocusScripts/listProjects.js +79 -0
  31. package/dist/utils/omnifocusScripts/taskNotifications.js +161 -0
  32. package/package.json +1 -1
  33. package/src/context/omnifocusData.ts +100 -0
  34. package/src/context/prompts.ts +194 -0
  35. package/src/context/resources.ts +80 -0
  36. package/src/server.ts +94 -1
  37. package/src/tools/definitions/addTag.ts +39 -0
  38. package/src/tools/definitions/addTaskNotification.ts +41 -0
  39. package/src/tools/definitions/appendToNote.ts +46 -0
  40. package/src/tools/definitions/countTasks.ts +45 -0
  41. package/src/tools/definitions/duplicateTask.ts +47 -0
  42. package/src/tools/definitions/editTag.ts +45 -0
  43. package/src/tools/definitions/listTaskNotifications.ts +54 -0
  44. package/src/tools/definitions/removeTag.ts +61 -0
  45. package/src/tools/definitions/removeTaskNotification.ts +41 -0
  46. package/src/tools/definitions/searchTags.ts +23 -0
  47. package/src/tools/primitives/addTag.ts +90 -0
  48. package/src/tools/primitives/appendToNote.test.ts +54 -0
  49. package/src/tools/primitives/appendToNote.ts +150 -0
  50. package/src/tools/primitives/countTasks.ts +53 -0
  51. package/src/tools/primitives/duplicateTask.test.ts +104 -0
  52. package/src/tools/primitives/duplicateTask.ts +51 -0
  53. package/src/tools/primitives/editTag.ts +229 -0
  54. package/src/tools/primitives/listProjects.test.ts +84 -0
  55. package/src/tools/primitives/removeTag.ts +121 -0
  56. package/src/tools/primitives/searchTags.ts +43 -0
  57. package/src/tools/primitives/tagCrud.test.ts +60 -0
  58. package/src/tools/primitives/taskNotifications.test.ts +134 -0
  59. package/src/tools/primitives/taskNotifications.ts +123 -0
  60. package/src/utils/omnifocusScripts/duplicateTask.js +76 -0
  61. package/src/utils/omnifocusScripts/listProjects.js +79 -0
  62. package/src/utils/omnifocusScripts/taskNotifications.js +161 -0
package/README.md CHANGED
@@ -44,6 +44,8 @@ Want to see where the project is heading next? See the [roadmap](docs/roadmap/20
44
44
 
45
45
  ## 🆕 Latest Release
46
46
 
47
+ - **v1.10.0** - Major capability expansion: **Tag Management** (`add_tag`, `edit_tag`, `remove_tag`, `search_tags`), **Task Notifications** (`list_task_notifications`, `add_task_notification`, `remove_task_notification` — absolute or due-relative reminders), plus first-class MCP **Prompts** (4 guided review workflows) and **Resources** (3 live JSON snapshots). Now 35 tools, 4 prompts, and 3 resources.
48
+ - **v1.9.0** - Added 3 productivity tools: `append_to_note` (append text to a task/project note without overwriting), `count_tasks` (fast "how many" aggregate queries with a status breakdown, using the same filters as `filter_tasks`), and `duplicate_task` (clone a task with or without its subtasks, optionally renamed).
47
49
  - **v1.8.0** - Added full **Folder Management** support: `add_folder`, `edit_folder`, `remove_folder`, `list_folders`, and `get_folder`. Create nested folder hierarchies, rename/move folders (with cycle protection), and inspect folder contents (child projects + subfolders). Note: removing a folder permanently deletes all projects and tasks it contains.
48
50
  - **v1.7.0** - Added OmniFocus 4.7+ repeat rule support via `set_repetition_rule` (ICS rule strings, schedule type, anchor date, catch-up, end date, repetition count), mutually exclusive tag support via `exclusiveTags` on add/edit tools, and improved planned-date editing tests.
49
51
  - **v1.6.10** - Fixed Inbox task completion via `edit_item`, fixed AppleScript special-character handling for apostrophes/backslashes, fixed JSON result escaping for special characters, and clarified `batch_add_items` / `mcporter` usage with working examples.
@@ -67,6 +69,10 @@ Want to see where the project is heading next? See the [roadmap](docs/roadmap/20
67
69
  - **📊 Smart Querying** - Find tasks by ID, name, or complex criteria
68
70
  - **🔄 Full CRUD Operations** - Create, read, update, delete tasks and projects
69
71
  - **📁 Folder Management** - Full CRUD for folders with nested hierarchy, move/rename, and content inspection
72
+ - **🏷️ Tag Management** - Full CRUD for tags with nesting, status control, and fuzzy search
73
+ - **🔔 Task Notifications** - List, add, and remove reminders (absolute time or relative to due date)
74
+ - **💬 MCP Prompts** - 4 guided review workflows (daily, weekly, inbox processing, project planning)
75
+ - **📡 MCP Resources** - 3 live JSON snapshots (inbox, today, active projects)
70
76
  - **📅 Time Management** - Due, defer, planned dates, estimates, and scheduling
71
77
  - **🏷️ Advanced Tagging** - Tag-based filtering with exact/partial matching
72
78
  - **🚫 Mutually Exclusive Tags** - Automatically respects exclusive tag groups when applying tags
@@ -521,8 +527,48 @@ read_task_attachment {
521
527
  21. **list_folders** - 🆕 **NEW**: List all folders with IDs, parents, status, and project counts
522
528
  22. **get_folder** - 🆕 **NEW**: Get a single folder with its child projects and subfolders
523
529
 
530
+ ### ⚡ Productivity Tools (NEW)
531
+ 23. **append_to_note** - 🆕 **NEW**: Append text to a task/project note without overwriting
532
+ 24. **count_tasks** - 🆕 **NEW**: Fast "how many" queries with a status breakdown (same filters as filter_tasks)
533
+ 25. **duplicate_task** - 🆕 **NEW**: Clone a task with/without subtasks, optionally renamed
534
+
535
+ ### 🏷️ Tag Management Tools (NEW)
536
+ 26. **list_tags** - List all tags with IDs, parents, and active status
537
+ 27. **add_tag** - 🆕 **NEW**: Create a tag, optionally nested under a parent tag
538
+ 28. **edit_tag** - 🆕 **NEW**: Rename, change status (active/onHold/dropped), or move a tag
539
+ 29. **remove_tag** - 🆕 **NEW**: Delete a tag (tasks are kept, they just lose the tag)
540
+ 30. **search_tags** - 🆕 **NEW**: Search tags by name (fuzzy or exact)
541
+
542
+ ### 🔔 Notification Tools (NEW)
543
+ 31. **list_task_notifications** - 🆕 **NEW**: List reminders set on a task
544
+ 32. **add_task_notification** - 🆕 **NEW**: Add a reminder (absolute time or minutes relative to due date)
545
+ 33. **remove_task_notification** - 🆕 **NEW**: Remove a reminder by index, or remove all
546
+
524
547
  ### 📊 Analytics & Tracking
525
- 23. **get_today_completed_tasks** - View today's completed tasks
548
+ 34. **get_today_completed_tasks** - View today's completed tasks
549
+
550
+ > Note: `get_tasks_by_tag` (#14) and `list_tags` (#26) were available in earlier versions; the tag CRUD tools are new in v1.10.0.
551
+
552
+ ## 💬 MCP Prompts (NEW in v1.10.0)
553
+
554
+ Guided review workflows that pull live OmniFocus data and hand the AI a structured plan of attack. In clients like Claude Desktop these appear as selectable prompts.
555
+
556
+ | Prompt | Arguments | What it does |
557
+ |---|---|---|
558
+ | **daily_review** | – | Pulls overdue, due-soon, and flagged tasks; produces today's top 3 priorities |
559
+ | **weekly_review** | – | GTD weekly review: classifies active projects as on track / at risk / stalled, proposes next actions |
560
+ | **inbox_processing** | – | Walks inbox items one by one through GTD clarification (delete/defer/delegate/keep) |
561
+ | **project_planning** | `project` | Breaks a project into sequenced, estimated next actions (fuzzy-matches the project name) |
562
+
563
+ ## 📡 MCP Resources (NEW in v1.10.0)
564
+
565
+ Live JSON snapshots your AI client can read without calling a tool.
566
+
567
+ | Resource URI | Contents |
568
+ |---|---|
569
+ | `omnifocus://inbox` | Current inbox tasks |
570
+ | `omnifocus://today` | Overdue + due today + flagged, grouped |
571
+ | `omnifocus://projects` | Active projects with task counts and stalled detection |
526
572
 
527
573
  Batch move feature roadmap (future): [docs/roadmap/2026-02-25-batch-move-tasks-plan.md](docs/roadmap/2026-02-25-batch-move-tasks-plan.md)
528
574
 
@@ -655,6 +701,78 @@ edit_folder {"name": "Key Clients", "newParentFolderName": ""}
655
701
  remove_folder {"name": "Old Archive"}
656
702
  ```
657
703
 
704
+ ### ⚡ Productivity Tools
705
+
706
+ ```bash
707
+ # Append a progress note without overwriting the existing note
708
+ append_to_note {
709
+ "itemType": "task",
710
+ "name": "Write report",
711
+ "text": "Drafted section 1 today"
712
+ }
713
+
714
+ # Fast count: how many flagged tasks are still actionable?
715
+ count_tasks {
716
+ "flagged": true,
717
+ "taskStatus": ["Available", "Next", "DueSoon", "Overdue"]
718
+ }
719
+
720
+ # How many tasks remain in a project (by status breakdown)
721
+ count_tasks {"projectFilter": "Website Redesign"}
722
+
723
+ # Duplicate a task template with its subtasks
724
+ duplicate_task {
725
+ "name": "Weekly Review Checklist",
726
+ "newName": "Weekly Review - 2026-03-02"
727
+ }
728
+
729
+ # Duplicate without subtasks
730
+ duplicate_task {"taskId": "abc123", "includeSubtasks": false}
731
+ ```
732
+
733
+ ### 🏷️ Tag Management
734
+
735
+ ```bash
736
+ # Search and list tags
737
+ search_tags {"query": "work"}
738
+ list_tags {"includeInactive": false}
739
+
740
+ # Create a tag, optionally nested
741
+ add_tag {"name": "Deep Work"}
742
+ add_tag {"name": "Client A", "parentTagName": "Clients"}
743
+
744
+ # Rename, pause, or move a tag ("" moves to root)
745
+ edit_tag {"name": "Deep Work", "newName": "Focus"}
746
+ edit_tag {"name": "Focus", "newStatus": "onHold"}
747
+ edit_tag {"name": "Client A", "newParentTagName": ""}
748
+
749
+ # Delete a tag (tasks are kept, they just lose the tag)
750
+ remove_tag {"name": "Obsolete"}
751
+ ```
752
+
753
+ ### 🔔 Task Notifications
754
+
755
+ ```bash
756
+ # See what reminders a task has
757
+ list_task_notifications {"taskName": "Submit report"}
758
+
759
+ # Remind at a fixed time
760
+ add_task_notification {
761
+ "taskName": "Submit report",
762
+ "absoluteDate": "2026-03-05T09:00:00"
763
+ }
764
+
765
+ # Remind 30 minutes before the due date (requires a due date)
766
+ add_task_notification {
767
+ "taskName": "Submit report",
768
+ "relativeMinutes": -30
769
+ }
770
+
771
+ # Remove one by index, or clear them all
772
+ remove_task_notification {"taskName": "Submit report", "index": 0}
773
+ remove_task_notification {"taskName": "Submit report", "removeAll": true}
774
+ ```
775
+
658
776
  ## 🔧 Configuration
659
777
 
660
778
  ### Claude Code
@@ -0,0 +1,60 @@
1
+ import { executeOmniFocusScript } from '../utils/scriptExecution.js';
2
+ import { applyClientSideFilters } from '../tools/primitives/filterTasks.js';
3
+ /**
4
+ * Fetch raw task data using the shared OmniJS filter script, applying the
5
+ * same client-side filters as filter_tasks. Returns structured objects
6
+ * (not formatted markdown) so prompts and resources can serialize them.
7
+ */
8
+ export async function fetchTasks(options = {}, limit = 1000) {
9
+ const result = await executeOmniFocusScript('@filterTasks.js', {
10
+ ...options,
11
+ perspective: options.perspective || 'all',
12
+ exactTagMatch: options.exactTagMatch ?? false,
13
+ limit: Math.max(limit * 5, 2000),
14
+ sortBy: options.sortBy || 'name',
15
+ sortOrder: options.sortOrder || 'asc'
16
+ });
17
+ if (!result || typeof result !== 'object') {
18
+ return [];
19
+ }
20
+ const data = result;
21
+ if (data.error) {
22
+ throw new Error(data.error);
23
+ }
24
+ const tasks = Array.isArray(data.tasks) ? data.tasks : [];
25
+ return applyClientSideFilters(tasks, options).slice(0, limit);
26
+ }
27
+ /**
28
+ * Compact a task down to the fields useful for planning prompts.
29
+ */
30
+ export function slimTask(task) {
31
+ return {
32
+ id: task.id,
33
+ name: task.name,
34
+ note: task.note || '',
35
+ status: task.taskStatus || null,
36
+ flagged: !!task.flagged,
37
+ dueDate: task.dueDate || null,
38
+ deferDate: task.deferDate || null,
39
+ plannedDate: task.plannedDate || null,
40
+ estimatedMinutes: task.estimatedMinutes ?? null,
41
+ projectName: task.projectName || null,
42
+ inInbox: !!task.inInbox,
43
+ tags: (task.tags || []).map(tag => tag.name)
44
+ };
45
+ }
46
+ /**
47
+ * Fetch projects with task counts and stalled detection.
48
+ * status: 'active' | 'onHold' | 'done' | 'dropped' | 'all'
49
+ */
50
+ export async function fetchProjects(status = 'active', limit = 500) {
51
+ const result = await executeOmniFocusScript('@listProjects.js', { status, limit });
52
+ const data = typeof result === 'string' ? JSON.parse(result) : result;
53
+ if (!data || typeof data !== 'object') {
54
+ return [];
55
+ }
56
+ if (data.success !== true) {
57
+ throw new Error(data.error || 'Unable to list OmniFocus projects');
58
+ }
59
+ return Array.isArray(data.projects) ? data.projects : [];
60
+ }
@@ -0,0 +1,156 @@
1
+ import { z } from 'zod';
2
+ import { fetchTasks, fetchProjects, slimTask } from './omnifocusData.js';
3
+ function userMessage(text) {
4
+ return {
5
+ messages: [
6
+ {
7
+ role: 'user',
8
+ content: { type: 'text', text }
9
+ }
10
+ ]
11
+ };
12
+ }
13
+ const ENGAGEMENT_PROTOCOL = `engagement protocol:
14
+ - ground every recommendation in the omnifocus data provided below.
15
+ - keep clarification questions minimal; only ask when genuinely blocked.
16
+ - after presenting the plan, ask whether to apply the changes in omnifocus now.
17
+ - if the user approves, execute the tool calls and report the created/updated ids.
18
+ - always ask for explicit confirmation before destructive operations
19
+ (remove_item, batch_remove_items, remove_folder, remove_tag).`;
20
+ export function registerPrompts(server) {
21
+ // 1. Daily review
22
+ server.prompt('daily_review', 'daily planning review with overdue, due-soon, and flagged tasks.', async () => {
23
+ const [overdue, dueSoon, flagged] = await Promise.all([
24
+ fetchTasks({ overdue: true, taskStatus: ['Overdue'] }, 50),
25
+ fetchTasks({ dueThisWeek: true, taskStatus: ['Available', 'Next', 'DueSoon'] }, 50),
26
+ fetchTasks({ flagged: true, taskStatus: ['Available', 'Next', 'DueSoon', 'Overdue', 'Blocked'] }, 50)
27
+ ]);
28
+ const text = `run a focused daily review using the omnifocus data below.
29
+
30
+ 1) identify the highest-risk overdue items.
31
+ 2) review due-soon tasks and sequence today's execution.
32
+ 3) evaluate flagged work and confirm whether it is genuinely urgent.
33
+ 4) produce exactly three top priorities for today, each with a short rationale.
34
+ 5) call out anything that should be deferred, delegated, or dropped.
35
+
36
+ ${ENGAGEMENT_PROTOCOL}
37
+
38
+ overdue_tasks_json:
39
+ ${JSON.stringify(overdue.map(slimTask))}
40
+
41
+ due_soon_tasks_json:
42
+ ${JSON.stringify(dueSoon.map(slimTask))}
43
+
44
+ flagged_tasks_json:
45
+ ${JSON.stringify(flagged.map(slimTask))}
46
+ `;
47
+ return userMessage(text);
48
+ });
49
+ // 2. Weekly review
50
+ server.prompt('weekly_review', 'gtd-style weekly review with active projects and next-action coverage.', async () => {
51
+ const [projects, availableTasks] = await Promise.all([
52
+ fetchProjects('active', 500),
53
+ fetchTasks({ taskStatus: ['Available', 'Next'] }, 1000)
54
+ ]);
55
+ const stalled = projects.filter(project => project.isStalled);
56
+ const text = `run a gtd-style weekly review using the omnifocus data below.
57
+
58
+ 1) review all active projects and classify each as: on track, at risk, or stalled.
59
+ 2) a project is stalled when work remains but no task is available or next.
60
+ the data already flags these with "isStalled": true.
61
+ 3) propose one concrete next action for every stalled project.
62
+ 4) highlight projects that need due/defer date changes or scope adjustments.
63
+ 5) produce a concise weekly plan:
64
+ - top 5 project priorities
65
+ - key risks and blockers
66
+ - cleanup actions (drop, defer, delegate, or someday/maybe)
67
+
68
+ ${ENGAGEMENT_PROTOCOL}
69
+
70
+ active_projects_json:
71
+ ${JSON.stringify(projects)}
72
+
73
+ stalled_projects_json:
74
+ ${JSON.stringify(stalled)}
75
+
76
+ available_tasks_json:
77
+ ${JSON.stringify(availableTasks.map(slimTask))}
78
+ `;
79
+ return userMessage(text);
80
+ });
81
+ // 3. Inbox processing
82
+ server.prompt('inbox_processing', 'gtd inbox processing session with one-by-one clarification decisions.', async () => {
83
+ const inboxTasks = await fetchTasks({ perspective: 'inbox' }, 200);
84
+ const text = `run a gtd inbox processing session using the inbox data below.
85
+
86
+ for each inbox item, guide a decision in this order:
87
+ 1) clarify the desired outcome and the very next action.
88
+ 2) decide whether to delete, defer, delegate, or keep it.
89
+ 3) if kept, assign the best target project (or keep in inbox if truly unassigned).
90
+ 4) propose relevant tags and whether it should be flagged.
91
+ 5) suggest due/defer dates only when there is a real deadline or start date.
92
+ 6) suggest estimated minutes when the task is actionable.
93
+
94
+ respond with:
95
+ - a prioritized processing queue
96
+ - concrete update recommendations per item
97
+ - a short batch action plan for the first five items
98
+
99
+ ${ENGAGEMENT_PROTOCOL}
100
+
101
+ inbox_items_json:
102
+ ${JSON.stringify(inboxTasks.map(slimTask))}
103
+ `;
104
+ return userMessage(text);
105
+ });
106
+ // 4. Project planning
107
+ server.prompt('project_planning', 'turn a project into clear, sequenced, executable next actions.', { project: z.string().describe('the name of the project to plan') }, async ({ project }) => {
108
+ const projectName = (project || '').trim();
109
+ if (projectName === '') {
110
+ throw new Error('project must not be empty.');
111
+ }
112
+ const allProjects = await fetchProjects('all', 1000);
113
+ const needle = projectName.toLowerCase();
114
+ const match = allProjects.find(candidate => candidate.name.toLowerCase() === needle) ||
115
+ allProjects.find(candidate => candidate.name.toLowerCase().includes(needle)) ||
116
+ null;
117
+ const projectTasks = match
118
+ ? await fetchTasks({ projectFilter: match.name, taskStatus: ['Available', 'Next', 'Blocked', 'DueSoon', 'Overdue'] }, 500)
119
+ : [];
120
+ const projectDetails = match
121
+ ? match
122
+ : { name: projectName, status: 'not_found', lookupNote: 'no matching project in omnifocus' };
123
+ const text = `plan this project into clear executable work.
124
+
125
+ project name:
126
+ ${projectName}
127
+
128
+ planning goals:
129
+ 1) summarize the project outcome in one concise sentence.
130
+ 2) evaluate current task coverage and identify missing steps.
131
+ 3) convert vague items into concrete next actions (verb-first, observable).
132
+ 4) sequence the work logically (dependencies first, then parallelizable actions).
133
+ 5) estimate effort in minutes for each next action and flag high-risk items.
134
+ 6) recommend what to do now, next, and later, plus what to defer or drop.
135
+
136
+ output format:
137
+ - project summary
138
+ - work breakdown table: action | estimate | priority | dependency | suggested tags | due/defer | rationale
139
+ - the first three actions to execute immediately
140
+ - risk and blocker list with mitigation ideas
141
+
142
+ notes:
143
+ - if the project status is "not_found", plan from user intent and then ask
144
+ whether to create the project in omnifocus.
145
+
146
+ ${ENGAGEMENT_PROTOCOL}
147
+
148
+ project_details_json:
149
+ ${JSON.stringify(projectDetails)}
150
+
151
+ project_tasks_json:
152
+ ${JSON.stringify(projectTasks.map(slimTask))}
153
+ `;
154
+ return userMessage(text);
155
+ });
156
+ }
@@ -0,0 +1,57 @@
1
+ import { fetchTasks, fetchProjects, slimTask } from './omnifocusData.js';
2
+ function jsonResource(uri, payload) {
3
+ return {
4
+ contents: [
5
+ {
6
+ uri,
7
+ mimeType: 'application/json',
8
+ text: JSON.stringify(payload, null, 2)
9
+ }
10
+ ]
11
+ };
12
+ }
13
+ export function registerResources(server) {
14
+ // 1. Inbox snapshot
15
+ server.resource('inbox', 'omnifocus://inbox', {
16
+ description: 'Current OmniFocus inbox tasks (uncompleted, unprocessed items).',
17
+ mimeType: 'application/json'
18
+ }, async () => {
19
+ const tasks = await fetchTasks({ perspective: 'inbox' }, 200);
20
+ return jsonResource('omnifocus://inbox', {
21
+ snapshotAt: new Date().toISOString(),
22
+ count: tasks.length,
23
+ tasks: tasks.map(slimTask)
24
+ });
25
+ });
26
+ // 2. Today snapshot (overdue + due today + flagged)
27
+ server.resource('today', 'omnifocus://today', {
28
+ description: "Today's OmniFocus focus: overdue tasks, tasks due today, and flagged tasks.",
29
+ mimeType: 'application/json'
30
+ }, async () => {
31
+ const [overdue, dueToday, flagged] = await Promise.all([
32
+ fetchTasks({ overdue: true, taskStatus: ['Overdue'] }, 100),
33
+ fetchTasks({ dueToday: true, taskStatus: ['Available', 'Next', 'DueSoon', 'Overdue'] }, 100),
34
+ fetchTasks({ flagged: true, taskStatus: ['Available', 'Next', 'DueSoon', 'Overdue', 'Blocked'] }, 100)
35
+ ]);
36
+ return jsonResource('omnifocus://today', {
37
+ snapshotAt: new Date().toISOString(),
38
+ overdue: { count: overdue.length, tasks: overdue.map(slimTask) },
39
+ dueToday: { count: dueToday.length, tasks: dueToday.map(slimTask) },
40
+ flagged: { count: flagged.length, tasks: flagged.map(slimTask) }
41
+ });
42
+ });
43
+ // 3. Active projects snapshot
44
+ server.resource('projects', 'omnifocus://projects', {
45
+ description: 'All active OmniFocus projects with task counts and stalled-project detection.',
46
+ mimeType: 'application/json'
47
+ }, async () => {
48
+ const projects = await fetchProjects('active', 500);
49
+ const stalled = projects.filter(project => project.isStalled);
50
+ return jsonResource('omnifocus://projects', {
51
+ snapshotAt: new Date().toISOString(),
52
+ count: projects.length,
53
+ stalledCount: stalled.length,
54
+ projects
55
+ });
56
+ });
57
+ }
package/dist/server.js CHANGED
@@ -31,10 +31,26 @@ import * as editFolderTool from './tools/definitions/editFolder.js';
31
31
  import * as removeFolderTool from './tools/definitions/removeFolder.js';
32
32
  import * as listFoldersTool from './tools/definitions/listFolders.js';
33
33
  import * as getFolderTool from './tools/definitions/getFolder.js';
34
+ // Import productivity tools
35
+ import * as appendToNoteTool from './tools/definitions/appendToNote.js';
36
+ import * as countTasksTool from './tools/definitions/countTasks.js';
37
+ import * as duplicateTaskTool from './tools/definitions/duplicateTask.js';
38
+ // Import tag management tools
39
+ import * as addTagTool from './tools/definitions/addTag.js';
40
+ import * as editTagTool from './tools/definitions/editTag.js';
41
+ import * as removeTagTool from './tools/definitions/removeTag.js';
42
+ import * as searchTagsTool from './tools/definitions/searchTags.js';
43
+ // Import notification tools
44
+ import * as listTaskNotificationsTool from './tools/definitions/listTaskNotifications.js';
45
+ import * as addTaskNotificationTool from './tools/definitions/addTaskNotification.js';
46
+ import * as removeTaskNotificationTool from './tools/definitions/removeTaskNotification.js';
47
+ // Import prompts and resources
48
+ import { registerPrompts } from './context/prompts.js';
49
+ import { registerResources } from './context/resources.js';
34
50
  // Create an MCP server
35
51
  const server = new McpServer({
36
52
  name: "OmniFocus MCP",
37
- version: "1.8.0"
53
+ version: "1.10.0"
38
54
  });
39
55
  // Register tools
40
56
  server.tool("dump_database", "Gets the current state of your OmniFocus database", dumpDatabaseTool.schema.shape, dumpDatabaseTool.handler);
@@ -66,6 +82,22 @@ server.tool("edit_folder", "Rename a folder or move it under a different parent
66
82
  server.tool("remove_folder", "Remove a folder from OmniFocus. WARNING: this also permanently deletes all projects and tasks contained in the folder.", removeFolderTool.schema.shape, removeFolderTool.handler);
67
83
  server.tool("list_folders", "List all OmniFocus folders with IDs, parent relationships, status, and project counts without loading tasks.", listFoldersTool.schema.shape, listFoldersTool.handler);
68
84
  server.tool("get_folder", "Get a single OmniFocus folder by ID or name, including its child projects and subfolders.", getFolderTool.schema.shape, getFolderTool.handler);
85
+ // Productivity tools
86
+ server.tool("append_to_note", "Append text to a task or project note without overwriting the existing note. Useful for logging progress or adding context.", appendToNoteTool.schema.shape, appendToNoteTool.handler);
87
+ server.tool("count_tasks", "Count tasks matching filters without returning the full list. Fast 'how many' queries that return a total plus a breakdown by status. Uses the same filters as filter_tasks.", countTasksTool.schema.shape, countTasksTool.handler);
88
+ server.tool("duplicate_task", "Duplicate an existing task, optionally with its subtasks, and optionally with a new name. Useful for template-based workflows.", duplicateTaskTool.schema.shape, duplicateTaskTool.handler);
89
+ // Tag management tools
90
+ server.tool("add_tag", "Create a new tag in OmniFocus, optionally nested under a parent tag.", addTagTool.schema.shape, addTagTool.handler);
91
+ server.tool("edit_tag", "Rename a tag, change its status (active/onHold/dropped), or move it under a different parent tag (empty string moves to root).", editTagTool.schema.shape, editTagTool.handler);
92
+ server.tool("remove_tag", "Delete a tag from OmniFocus. Tasks are not deleted; they simply lose the tag. Child tags are deleted with the parent.", removeTagTool.schema.shape, removeTagTool.handler);
93
+ server.tool("search_tags", "Search OmniFocus tags by name with fuzzy or exact matching.", searchTagsTool.schema.shape, searchTagsTool.handler);
94
+ // Notification tools
95
+ server.tool("list_task_notifications", "List all notifications (reminders) set on a task, with their kind and fire time.", listTaskNotificationsTool.schema.shape, listTaskNotificationsTool.handler);
96
+ server.tool("add_task_notification", "Add a notification (reminder) to a task. Use absoluteDate for a fixed time, or relativeMinutes for an offset from the task's due date (negative = before due).", addTaskNotificationTool.schema.shape, addTaskNotificationTool.handler);
97
+ server.tool("remove_task_notification", "Remove a notification from a task by 0-based index, or remove all notifications with removeAll.", removeTaskNotificationTool.schema.shape, removeTaskNotificationTool.handler);
98
+ // Register prompts (guided review workflows) and resources (live snapshots)
99
+ registerPrompts(server);
100
+ registerResources(server);
69
101
  // Start the MCP server
70
102
  const transport = new StdioServerTransport();
71
103
  // Use await with server.connect to ensure proper connection
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ import { addTag } from '../primitives/addTag.js';
3
+ export const schema = z.object({
4
+ name: z.string().describe('The name of the tag'),
5
+ parentTagName: z.string().optional().describe('The name of the parent tag to nest this tag under (created at the root level if not specified)')
6
+ });
7
+ export async function handler(args, _extra) {
8
+ try {
9
+ const result = await addTag(args);
10
+ if (result.success) {
11
+ const locationText = args.parentTagName
12
+ ? `under tag "${args.parentTagName}"`
13
+ : 'at the root level';
14
+ return {
15
+ content: [{
16
+ type: 'text',
17
+ text: `✅ Tag "${args.name}" created successfully ${locationText}.\n\nid: ${result.tagId}`
18
+ }]
19
+ };
20
+ }
21
+ return {
22
+ content: [{ type: 'text', text: `Failed to create tag: ${result.error}` }],
23
+ isError: true
24
+ };
25
+ }
26
+ catch (err) {
27
+ const error = err;
28
+ console.error(`Tool execution error: ${error.message}`);
29
+ return {
30
+ content: [{ type: 'text', text: `Error creating tag: ${error.message}` }],
31
+ isError: true
32
+ };
33
+ }
34
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ import { addTaskNotification, formatNotification } from '../primitives/taskNotifications.js';
3
+ export const schema = z.object({
4
+ taskId: z.string().optional().describe('The ID of the task'),
5
+ taskName: z.string().optional().describe('The name of the task (as fallback if ID not provided)'),
6
+ absoluteDate: z.string().optional().describe('Absolute notification time in ISO 8601 format (e.g. 2026-03-05T09:00:00). Use this OR relativeMinutes, not both.'),
7
+ relativeMinutes: z.number().optional().describe('Minutes relative to the task due date. Negative means before the due date (e.g. -30 = 30 minutes before). Requires the task to have a due date.')
8
+ });
9
+ export async function handler(args, _extra) {
10
+ try {
11
+ const result = await addTaskNotification(args);
12
+ if (!result.success) {
13
+ return {
14
+ content: [{ type: 'text', text: `Failed to add notification: ${result.error}` }],
15
+ isError: true
16
+ };
17
+ }
18
+ const total = (result.notifications || []).length;
19
+ const addedText = result.added ? formatNotification(result.added) : 'notification added';
20
+ return {
21
+ content: [{
22
+ type: 'text',
23
+ text: `✅ Added notification to "${result.taskName}".\n${addedText}\n\nTotal notifications: ${total}`
24
+ }]
25
+ };
26
+ }
27
+ catch (err) {
28
+ const error = err;
29
+ console.error(`Tool execution error: ${error.message}`);
30
+ return {
31
+ content: [{ type: 'text', text: `Error adding notification: ${error.message}` }],
32
+ isError: true
33
+ };
34
+ }
35
+ }
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod';
2
+ import { appendToNote } from '../primitives/appendToNote.js';
3
+ export const schema = z.object({
4
+ id: z.string().optional().describe('The ID of the task or project'),
5
+ name: z.string().optional().describe('The name of the task or project (as fallback if ID not provided)'),
6
+ itemType: z.enum(['task', 'project']).describe("Type of item whose note to append to ('task' or 'project')"),
7
+ text: z.string().describe('The text to append to the existing note'),
8
+ separator: z.string().optional().describe('Separator inserted between the existing note and the new text (default: a newline). Pass an empty string to append with no separator.')
9
+ });
10
+ export async function handler(args, _extra) {
11
+ try {
12
+ if (!args.id && !args.name) {
13
+ return {
14
+ content: [{ type: 'text', text: 'Either id or name must be provided to append to a note.' }],
15
+ isError: true
16
+ };
17
+ }
18
+ const result = await appendToNote(args);
19
+ if (result.success) {
20
+ const itemTypeLabel = args.itemType === 'task' ? 'Task' : 'Project';
21
+ return {
22
+ content: [{
23
+ type: 'text',
24
+ text: `✅ Appended text to ${itemTypeLabel} "${result.name}" note.\n\nid: ${result.id}`
25
+ }]
26
+ };
27
+ }
28
+ return {
29
+ content: [{ type: 'text', text: `Failed to append to note: ${result.error}` }],
30
+ isError: true
31
+ };
32
+ }
33
+ catch (err) {
34
+ const error = err;
35
+ console.error(`Tool execution error: ${error.message}`);
36
+ return {
37
+ content: [{ type: 'text', text: `Error appending to note: ${error.message}` }],
38
+ isError: true
39
+ };
40
+ }
41
+ }
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod';
2
+ import { countTasks } from '../primitives/countTasks.js';
3
+ export const schema = z.object({
4
+ taskStatus: z.array(z.string()).optional().describe('Filter by task status: Available, Next, Blocked, DueSoon, Overdue, Completed, Dropped'),
5
+ perspective: z.enum(['inbox', 'flagged', 'all']).optional().describe("Scope: 'inbox', 'flagged', or 'all' (default: all)"),
6
+ projectFilter: z.string().optional().describe('Only count tasks in projects whose name contains this text'),
7
+ tagFilter: z.union([z.string(), z.array(z.string())]).optional().describe('Only count tasks with these tags'),
8
+ exactTagMatch: z.boolean().optional().describe('Require exact tag name match (default: false)'),
9
+ flagged: z.boolean().optional().describe('Only count flagged (true) or unflagged (false) tasks'),
10
+ searchText: z.string().optional().describe('Only count tasks whose name or note contains this text'),
11
+ dueToday: z.boolean().optional().describe('Only count tasks due today'),
12
+ dueThisWeek: z.boolean().optional().describe('Only count tasks due this week'),
13
+ overdue: z.boolean().optional().describe('Only count overdue tasks'),
14
+ completedToday: z.boolean().optional().describe('Only count tasks completed today'),
15
+ completedThisWeek: z.boolean().optional().describe('Only count tasks completed this week'),
16
+ plannedToday: z.boolean().optional().describe('Only count tasks planned for today'),
17
+ plannedThisWeek: z.boolean().optional().describe('Only count tasks planned for this week')
18
+ });
19
+ export async function handler(args, _extra) {
20
+ try {
21
+ const result = await countTasks(args);
22
+ const statusEntries = Object.entries(result.byStatus).sort((a, b) => b[1] - a[1]);
23
+ const breakdown = statusEntries.length > 0
24
+ ? statusEntries.map(([status, count]) => `- ${status}: ${count}`).join('\n')
25
+ : '- (no matching tasks)';
26
+ return {
27
+ content: [{
28
+ type: 'text',
29
+ text: `# Task Count\n\n**Total: ${result.total}**\n\nBy status:\n${breakdown}`
30
+ }]
31
+ };
32
+ }
33
+ catch (err) {
34
+ const error = err;
35
+ console.error(`Tool execution error: ${error.message}`);
36
+ return {
37
+ content: [{ type: 'text', text: `Error counting tasks: ${error.message}` }],
38
+ isError: true
39
+ };
40
+ }
41
+ }