omnifocus-mcp-enhanced 1.9.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.
- package/README.md +84 -1
- package/dist/context/omnifocusData.js +60 -0
- package/dist/context/prompts.js +156 -0
- package/dist/context/resources.js +57 -0
- package/dist/server.js +25 -1
- package/dist/tools/definitions/addTag.js +34 -0
- package/dist/tools/definitions/addTaskNotification.js +35 -0
- package/dist/tools/definitions/editTag.js +40 -0
- package/dist/tools/definitions/listTaskNotifications.js +47 -0
- package/dist/tools/definitions/removeTag.js +59 -0
- package/dist/tools/definitions/removeTaskNotification.js +35 -0
- package/dist/tools/definitions/searchTags.js +20 -0
- package/dist/tools/primitives/addTag.js +77 -0
- package/dist/tools/primitives/editTag.js +194 -0
- package/dist/tools/primitives/listProjects.test.js +67 -0
- package/dist/tools/primitives/removeTag.js +97 -0
- package/dist/tools/primitives/searchTags.js +30 -0
- package/dist/tools/primitives/tagCrud.test.js +47 -0
- package/dist/tools/primitives/taskNotifications.js +78 -0
- package/dist/tools/primitives/taskNotifications.test.js +105 -0
- package/dist/utils/omnifocusScripts/listProjects.js +79 -0
- package/dist/utils/omnifocusScripts/taskNotifications.js +161 -0
- package/package.json +1 -1
- package/src/context/omnifocusData.ts +100 -0
- package/src/context/prompts.ts +194 -0
- package/src/context/resources.ts +80 -0
- package/src/server.ts +68 -1
- package/src/tools/definitions/addTag.ts +39 -0
- package/src/tools/definitions/addTaskNotification.ts +41 -0
- package/src/tools/definitions/editTag.ts +45 -0
- package/src/tools/definitions/listTaskNotifications.ts +54 -0
- package/src/tools/definitions/removeTag.ts +61 -0
- package/src/tools/definitions/removeTaskNotification.ts +41 -0
- package/src/tools/definitions/searchTags.ts +23 -0
- package/src/tools/primitives/addTag.ts +90 -0
- package/src/tools/primitives/editTag.ts +229 -0
- package/src/tools/primitives/listProjects.test.ts +84 -0
- package/src/tools/primitives/removeTag.ts +121 -0
- package/src/tools/primitives/searchTags.ts +43 -0
- package/src/tools/primitives/tagCrud.test.ts +60 -0
- package/src/tools/primitives/taskNotifications.test.ts +134 -0
- package/src/tools/primitives/taskNotifications.ts +123 -0
- package/src/utils/omnifocusScripts/listProjects.js +79 -0
- package/src/utils/omnifocusScripts/taskNotifications.js +161 -0
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ 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.
|
|
47
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).
|
|
48
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.
|
|
49
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.
|
|
@@ -68,6 +69,10 @@ Want to see where the project is heading next? See the [roadmap](docs/roadmap/20
|
|
|
68
69
|
- **📊 Smart Querying** - Find tasks by ID, name, or complex criteria
|
|
69
70
|
- **🔄 Full CRUD Operations** - Create, read, update, delete tasks and projects
|
|
70
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)
|
|
71
76
|
- **📅 Time Management** - Due, defer, planned dates, estimates, and scheduling
|
|
72
77
|
- **🏷️ Advanced Tagging** - Tag-based filtering with exact/partial matching
|
|
73
78
|
- **🚫 Mutually Exclusive Tags** - Automatically respects exclusive tag groups when applying tags
|
|
@@ -527,8 +532,43 @@ read_task_attachment {
|
|
|
527
532
|
24. **count_tasks** - 🆕 **NEW**: Fast "how many" queries with a status breakdown (same filters as filter_tasks)
|
|
528
533
|
25. **duplicate_task** - 🆕 **NEW**: Clone a task with/without subtasks, optionally renamed
|
|
529
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
|
+
|
|
530
547
|
### 📊 Analytics & Tracking
|
|
531
|
-
|
|
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 |
|
|
532
572
|
|
|
533
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)
|
|
534
574
|
|
|
@@ -690,6 +730,49 @@ duplicate_task {
|
|
|
690
730
|
duplicate_task {"taskId": "abc123", "includeSubtasks": false}
|
|
691
731
|
```
|
|
692
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
|
+
|
|
693
776
|
## 🔧 Configuration
|
|
694
777
|
|
|
695
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
|
@@ -35,10 +35,22 @@ import * as getFolderTool from './tools/definitions/getFolder.js';
|
|
|
35
35
|
import * as appendToNoteTool from './tools/definitions/appendToNote.js';
|
|
36
36
|
import * as countTasksTool from './tools/definitions/countTasks.js';
|
|
37
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';
|
|
38
50
|
// Create an MCP server
|
|
39
51
|
const server = new McpServer({
|
|
40
52
|
name: "OmniFocus MCP",
|
|
41
|
-
version: "1.
|
|
53
|
+
version: "1.10.0"
|
|
42
54
|
});
|
|
43
55
|
// Register tools
|
|
44
56
|
server.tool("dump_database", "Gets the current state of your OmniFocus database", dumpDatabaseTool.schema.shape, dumpDatabaseTool.handler);
|
|
@@ -74,6 +86,18 @@ server.tool("get_folder", "Get a single OmniFocus folder by ID or name, includin
|
|
|
74
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);
|
|
75
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);
|
|
76
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);
|
|
77
101
|
// Start the MCP server
|
|
78
102
|
const transport = new StdioServerTransport();
|
|
79
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,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { editTag } from '../primitives/editTag.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
id: z.string().optional().describe('The ID of the tag to edit'),
|
|
5
|
+
name: z.string().optional().describe('The name of the tag to edit (as fallback if ID not provided)'),
|
|
6
|
+
newName: z.string().optional().describe('New name for the tag'),
|
|
7
|
+
newStatus: z.enum(['active', 'onHold', 'dropped']).optional().describe("New status: 'active' (available), 'onHold' (does not allow next actions), or 'dropped' (hidden)"),
|
|
8
|
+
newParentTagName: z.string().optional().describe('Move the tag under this parent tag. Use an empty string "" to move it to the root level.')
|
|
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 edit a tag.' }],
|
|
15
|
+
isError: true
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const result = await editTag(args);
|
|
19
|
+
if (result.success) {
|
|
20
|
+
return {
|
|
21
|
+
content: [{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: `✅ Tag "${result.name}" updated successfully.\nChanged: ${result.changedProperties || 'nothing'}\n\nid: ${result.id}`
|
|
24
|
+
}]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: 'text', text: `Failed to edit tag: ${result.error}` }],
|
|
29
|
+
isError: true
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const error = err;
|
|
34
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: 'text', text: `Error editing tag: ${error.message}` }],
|
|
37
|
+
isError: true
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { listTaskNotifications, 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
|
+
});
|
|
7
|
+
export async function handler(args, _extra) {
|
|
8
|
+
try {
|
|
9
|
+
if (!args.taskId && !args.taskName) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: 'text', text: 'Either taskId or taskName must be provided.' }],
|
|
12
|
+
isError: true
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const result = await listTaskNotifications(args);
|
|
16
|
+
if (!result.success) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: 'text', text: `Failed to list notifications: ${result.error}` }],
|
|
19
|
+
isError: true
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const notifications = result.notifications || [];
|
|
23
|
+
if (notifications.length === 0) {
|
|
24
|
+
return {
|
|
25
|
+
content: [{
|
|
26
|
+
type: 'text',
|
|
27
|
+
text: `# Notifications for "${result.taskName}"\n\nNo notifications set.`
|
|
28
|
+
}]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const lines = notifications.map(formatNotification);
|
|
32
|
+
return {
|
|
33
|
+
content: [{
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: `# Notifications for "${result.taskName}" (${notifications.length})\n\n${lines.join('\n')}`
|
|
36
|
+
}]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const error = err;
|
|
41
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: 'text', text: `Error listing notifications: ${error.message}` }],
|
|
44
|
+
isError: true
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { removeTag } from '../primitives/removeTag.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
id: z.string().optional().describe('The ID of the tag to remove'),
|
|
5
|
+
name: z.string().optional().describe('The name of the tag to remove (as fallback if ID not provided)')
|
|
6
|
+
});
|
|
7
|
+
export async function handler(args, _extra) {
|
|
8
|
+
try {
|
|
9
|
+
if (!args.id && !args.name) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: 'text', text: 'Either id or name must be provided to remove a tag.' }],
|
|
12
|
+
isError: true
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const result = await removeTag(args);
|
|
16
|
+
if (result.success) {
|
|
17
|
+
const taskCount = result.affectedTaskCount ?? 0;
|
|
18
|
+
const childCount = result.childTagCount ?? 0;
|
|
19
|
+
const details = [];
|
|
20
|
+
if (taskCount > 0)
|
|
21
|
+
details.push(`removed from ${taskCount} task(s)`);
|
|
22
|
+
if (childCount > 0)
|
|
23
|
+
details.push(`⚠️ also deleted ${childCount} child tag(s)`);
|
|
24
|
+
const detailText = details.length > 0 ? `\n${details.join(', ')}.` : '';
|
|
25
|
+
return {
|
|
26
|
+
content: [{
|
|
27
|
+
type: 'text',
|
|
28
|
+
text: `✅ Tag "${result.name}" removed successfully.${detailText}\n\nTasks themselves were not deleted.`
|
|
29
|
+
}]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
let errorMsg = 'Failed to remove tag';
|
|
33
|
+
if (result.error) {
|
|
34
|
+
if (result.error.includes('Tag not found')) {
|
|
35
|
+
errorMsg = 'Tag not found';
|
|
36
|
+
if (args.id)
|
|
37
|
+
errorMsg += ` with ID "${args.id}"`;
|
|
38
|
+
if (args.name)
|
|
39
|
+
errorMsg += `${args.id ? ' or' : ' with'} name "${args.name}"`;
|
|
40
|
+
errorMsg += '.';
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
errorMsg += `: ${result.error}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: 'text', text: errorMsg }],
|
|
48
|
+
isError: true
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const error = err;
|
|
53
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: 'text', text: `Error removing tag: ${error.message}` }],
|
|
56
|
+
isError: true
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|