@iflow-mcp/omnifocus-mcp 1.2.3
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/QUERY_TOOL_EXAMPLES.md +298 -0
- package/QUERY_TOOL_REFERENCE.md +228 -0
- package/README.md +250 -0
- package/assets/omnifocus-mcp-logo.png +0 -0
- package/cli.cjs +9 -0
- package/dist/omnifocustypes.js +48 -0
- package/dist/server.js +44 -0
- package/dist/tools/definitions/addOmniFocusTask.js +76 -0
- package/dist/tools/definitions/addProject.js +61 -0
- package/dist/tools/definitions/batchAddItems.js +89 -0
- package/dist/tools/definitions/batchRemoveItems.js +74 -0
- package/dist/tools/definitions/dumpDatabase.js +259 -0
- package/dist/tools/definitions/editItem.js +88 -0
- package/dist/tools/definitions/getPerspectiveView.js +107 -0
- package/dist/tools/definitions/listPerspectives.js +65 -0
- package/dist/tools/definitions/queryOmnifocus.js +190 -0
- package/dist/tools/definitions/removeItem.js +80 -0
- package/dist/tools/dumpDatabase.js +121 -0
- package/dist/tools/dumpDatabaseOptimized.js +192 -0
- package/dist/tools/primitives/addOmniFocusTask.js +227 -0
- package/dist/tools/primitives/addProject.js +132 -0
- package/dist/tools/primitives/batchAddItems.js +166 -0
- package/dist/tools/primitives/batchRemoveItems.js +44 -0
- package/dist/tools/primitives/editItem.js +443 -0
- package/dist/tools/primitives/getPerspectiveView.js +50 -0
- package/dist/tools/primitives/listPerspectives.js +34 -0
- package/dist/tools/primitives/queryOmnifocus.js +365 -0
- package/dist/tools/primitives/queryOmnifocusDebug.js +135 -0
- package/dist/tools/primitives/removeItem.js +177 -0
- package/dist/types.js +1 -0
- package/dist/utils/cacheManager.js +187 -0
- package/dist/utils/dateFormatting.js +58 -0
- package/dist/utils/omnifocusScripts/getPerspectiveView.js +169 -0
- package/dist/utils/omnifocusScripts/listPerspectives.js +59 -0
- package/dist/utils/omnifocusScripts/omnifocusDump.js +223 -0
- package/dist/utils/scriptExecution.js +113 -0
- package/package.json +37 -0
- package/src/omnifocustypes.ts +89 -0
- package/src/server.ts +109 -0
- package/src/tools/definitions/addOmniFocusTask.ts +80 -0
- package/src/tools/definitions/addProject.ts +67 -0
- package/src/tools/definitions/batchAddItems.ts +98 -0
- package/src/tools/definitions/batchRemoveItems.ts +80 -0
- package/src/tools/definitions/dumpDatabase.ts +311 -0
- package/src/tools/definitions/editItem.ts +96 -0
- package/src/tools/definitions/getPerspectiveView.ts +125 -0
- package/src/tools/definitions/listPerspectives.ts +72 -0
- package/src/tools/definitions/queryOmnifocus.ts +212 -0
- package/src/tools/definitions/removeItem.ts +86 -0
- package/src/tools/dumpDatabase.ts +196 -0
- package/src/tools/dumpDatabaseOptimized.ts +231 -0
- package/src/tools/primitives/addOmniFocusTask.ts +252 -0
- package/src/tools/primitives/addProject.ts +156 -0
- package/src/tools/primitives/batchAddItems.ts +207 -0
- package/src/tools/primitives/batchRemoveItems.ts +64 -0
- package/src/tools/primitives/editItem.ts +507 -0
- package/src/tools/primitives/getPerspectiveView.ts +71 -0
- package/src/tools/primitives/listPerspectives.ts +53 -0
- package/src/tools/primitives/queryOmnifocus.ts +394 -0
- package/src/tools/primitives/queryOmnifocusDebug.ts +139 -0
- package/src/tools/primitives/removeItem.ts +195 -0
- package/src/types.ts +107 -0
- package/src/utils/cacheManager.ts +234 -0
- package/src/utils/dateFormatting.ts +81 -0
- package/src/utils/omnifocusScripts/getPerspectiveView.js +169 -0
- package/src/utils/omnifocusScripts/listPerspectives.js +59 -0
- package/src/utils/omnifocusScripts/omnifocusDump.js +223 -0
- package/src/utils/scriptExecution.ts +128 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { dumpDatabase } from '../dumpDatabase.js';
|
|
3
|
+
import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
4
|
+
|
|
5
|
+
export const schema = z.object({
|
|
6
|
+
hideCompleted: z.boolean().optional().describe("Set to false to show completed and dropped tasks (default: true)"),
|
|
7
|
+
hideRecurringDuplicates: z.boolean().optional().describe("Set to true to hide duplicate instances of recurring tasks (default: true)")
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export async function handler(args: z.infer<typeof schema>, extra: RequestHandlerExtra) {
|
|
11
|
+
try {
|
|
12
|
+
// Get raw database
|
|
13
|
+
const database = await dumpDatabase();
|
|
14
|
+
|
|
15
|
+
// Format as compact report
|
|
16
|
+
const formattedReport = formatCompactReport(database, {
|
|
17
|
+
hideCompleted: args.hideCompleted !== false, // Default to true
|
|
18
|
+
hideRecurringDuplicates: args.hideRecurringDuplicates !== false // Default to true
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
content: [{
|
|
23
|
+
type: "text" as const,
|
|
24
|
+
text: formattedReport
|
|
25
|
+
}]
|
|
26
|
+
};
|
|
27
|
+
} catch (err: unknown) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
type: "text" as const,
|
|
31
|
+
text: `Error generating report. Please ensure OmniFocus is running and try again.`
|
|
32
|
+
}],
|
|
33
|
+
isError: true
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Function to format date in compact format (M/D)
|
|
39
|
+
function formatCompactDate(isoDate: string | null): string {
|
|
40
|
+
if (!isoDate) return '';
|
|
41
|
+
|
|
42
|
+
const date = new Date(isoDate);
|
|
43
|
+
return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Function to format the database in the compact report format
|
|
47
|
+
function formatCompactReport(database: any, options: { hideCompleted: boolean, hideRecurringDuplicates: boolean }): string {
|
|
48
|
+
const { hideCompleted, hideRecurringDuplicates } = options;
|
|
49
|
+
|
|
50
|
+
// Get current date for the header
|
|
51
|
+
const today = new Date();
|
|
52
|
+
const dateStr = today.toISOString().split('T')[0];
|
|
53
|
+
|
|
54
|
+
let output = `# OMNIFOCUS [${dateStr}]\n\n`;
|
|
55
|
+
|
|
56
|
+
// Add legend
|
|
57
|
+
output += `FORMAT LEGEND:
|
|
58
|
+
F: Folder | P: Project | •: Task | 🚩: Flagged
|
|
59
|
+
Dates: [M/D] | Duration: (30m) or (2h) | Tags: <tag1,tag2>
|
|
60
|
+
Status: #next #avail #block #due #over #compl #drop\n\n`;
|
|
61
|
+
|
|
62
|
+
// Map of folder IDs to folder objects for quick lookup
|
|
63
|
+
const folderMap = new Map();
|
|
64
|
+
Object.values(database.folders).forEach((folder: any) => {
|
|
65
|
+
folderMap.set(folder.id, folder);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Get all tag names to compute minimum unique prefixes
|
|
69
|
+
const allTagNames = Object.values(database.tags).map((tag: any) => tag.name);
|
|
70
|
+
const tagPrefixMap = computeMinimumUniquePrefixes(allTagNames);
|
|
71
|
+
|
|
72
|
+
// Function to get folder hierarchy path
|
|
73
|
+
function getFolderPath(folderId: string): string[] {
|
|
74
|
+
const path = [];
|
|
75
|
+
let currentId = folderId;
|
|
76
|
+
|
|
77
|
+
while (currentId) {
|
|
78
|
+
const folder = folderMap.get(currentId);
|
|
79
|
+
if (!folder) break;
|
|
80
|
+
|
|
81
|
+
path.unshift(folder.name);
|
|
82
|
+
currentId = folder.parentFolderID;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return path;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Get root folders (no parent)
|
|
89
|
+
const rootFolders = Object.values(database.folders).filter((folder: any) => !folder.parentFolderID);
|
|
90
|
+
|
|
91
|
+
// Process folders recursively
|
|
92
|
+
function processFolder(folder: any, level: number): string {
|
|
93
|
+
const indent = ' '.repeat(level);
|
|
94
|
+
let folderOutput = `${indent}F: ${folder.name}\n`;
|
|
95
|
+
|
|
96
|
+
// Process subfolders
|
|
97
|
+
if (folder.subfolders && folder.subfolders.length > 0) {
|
|
98
|
+
for (const subfolderId of folder.subfolders) {
|
|
99
|
+
const subfolder = database.folders[subfolderId];
|
|
100
|
+
if (subfolder) {
|
|
101
|
+
folderOutput += `${processFolder(subfolder, level + 1)}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Process projects in this folder
|
|
107
|
+
if (folder.projects && folder.projects.length > 0) {
|
|
108
|
+
for (const projectId of folder.projects) {
|
|
109
|
+
const project = database.projects[projectId];
|
|
110
|
+
if (project) {
|
|
111
|
+
folderOutput += processProject(project, level + 1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return folderOutput;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Process a project
|
|
120
|
+
function processProject(project: any, level: number): string {
|
|
121
|
+
const indent = ' '.repeat(level);
|
|
122
|
+
|
|
123
|
+
// Skip if it's completed or dropped and we're hiding completed items
|
|
124
|
+
if (hideCompleted && (project.status === 'Done' || project.status === 'Dropped')) {
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Format project status info
|
|
129
|
+
let statusInfo = '';
|
|
130
|
+
if (project.status === 'OnHold') {
|
|
131
|
+
statusInfo = ' [OnHold]';
|
|
132
|
+
} else if (project.status === 'Dropped') {
|
|
133
|
+
statusInfo = ' [Dropped]';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add due date if present
|
|
137
|
+
if (project.dueDate) {
|
|
138
|
+
const dueDateStr = formatCompactDate(project.dueDate);
|
|
139
|
+
statusInfo += statusInfo ? ` [DUE:${dueDateStr}]` : ` [DUE:${dueDateStr}]`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add flag if present
|
|
143
|
+
const flaggedSymbol = project.flagged ? ' 🚩' : '';
|
|
144
|
+
|
|
145
|
+
let projectOutput = `${indent}P: ${project.name}${flaggedSymbol}${statusInfo}\n`;
|
|
146
|
+
|
|
147
|
+
// Process tasks in this project
|
|
148
|
+
const projectTasks = database.tasks.filter((task: any) =>
|
|
149
|
+
task.projectId === project.id && !task.parentId
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (projectTasks.length > 0) {
|
|
153
|
+
for (const task of projectTasks) {
|
|
154
|
+
projectOutput += processTask(task, level + 1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return projectOutput;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Process a task
|
|
162
|
+
function processTask(task: any, level: number): string {
|
|
163
|
+
const indent = ' '.repeat(level);
|
|
164
|
+
|
|
165
|
+
// Skip if it's completed or dropped and we're hiding completed items
|
|
166
|
+
if (hideCompleted && (task.completed || task.taskStatus === 'Completed' || task.taskStatus === 'Dropped')) {
|
|
167
|
+
return '';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Flag symbol
|
|
171
|
+
const flagSymbol = task.flagged ? '🚩 ' : '';
|
|
172
|
+
|
|
173
|
+
// Format dates
|
|
174
|
+
let dateInfo = '';
|
|
175
|
+
if (task.dueDate) {
|
|
176
|
+
const dueDateStr = formatCompactDate(task.dueDate);
|
|
177
|
+
dateInfo += ` [DUE:${dueDateStr}]`;
|
|
178
|
+
}
|
|
179
|
+
if (task.deferDate) {
|
|
180
|
+
const deferDateStr = formatCompactDate(task.deferDate);
|
|
181
|
+
dateInfo += ` [defer:${deferDateStr}]`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Format duration
|
|
185
|
+
let durationStr = '';
|
|
186
|
+
if (task.estimatedMinutes) {
|
|
187
|
+
// Convert to hours if >= 60 minutes
|
|
188
|
+
if (task.estimatedMinutes >= 60) {
|
|
189
|
+
const hours = Math.floor(task.estimatedMinutes / 60);
|
|
190
|
+
durationStr = ` (${hours}h)`;
|
|
191
|
+
} else {
|
|
192
|
+
durationStr = ` (${task.estimatedMinutes}m)`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Format tags
|
|
197
|
+
let tagsStr = '';
|
|
198
|
+
if (task.tagNames && task.tagNames.length > 0) {
|
|
199
|
+
// Use minimum unique prefixes for tag names
|
|
200
|
+
const abbreviatedTags = task.tagNames.map((tag: string) => {
|
|
201
|
+
return tagPrefixMap.get(tag) || tag;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
tagsStr = ` <${abbreviatedTags.join(',')}>`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Format status
|
|
208
|
+
let statusStr = '';
|
|
209
|
+
switch (task.taskStatus) {
|
|
210
|
+
case 'Next':
|
|
211
|
+
statusStr = ' #next';
|
|
212
|
+
break;
|
|
213
|
+
case 'Available':
|
|
214
|
+
statusStr = ' #avail';
|
|
215
|
+
break;
|
|
216
|
+
case 'Blocked':
|
|
217
|
+
statusStr = ' #block';
|
|
218
|
+
break;
|
|
219
|
+
case 'DueSoon':
|
|
220
|
+
statusStr = ' #due';
|
|
221
|
+
break;
|
|
222
|
+
case 'Overdue':
|
|
223
|
+
statusStr = ' #over';
|
|
224
|
+
break;
|
|
225
|
+
case 'Completed':
|
|
226
|
+
statusStr = ' #compl';
|
|
227
|
+
break;
|
|
228
|
+
case 'Dropped':
|
|
229
|
+
statusStr = ' #drop';
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let taskOutput = `${indent}• ${flagSymbol}${task.name}${dateInfo}${durationStr}${tagsStr}${statusStr}\n`;
|
|
234
|
+
|
|
235
|
+
// Process subtasks
|
|
236
|
+
if (task.childIds && task.childIds.length > 0) {
|
|
237
|
+
const childTasks = database.tasks.filter((t: any) => task.childIds.includes(t.id));
|
|
238
|
+
|
|
239
|
+
for (const childTask of childTasks) {
|
|
240
|
+
taskOutput += processTask(childTask, level + 1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return taskOutput;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Process all root folders
|
|
248
|
+
for (const folder of rootFolders) {
|
|
249
|
+
output += processFolder(folder, 0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Process projects not in any folder (if any)
|
|
253
|
+
const rootProjects = Object.values(database.projects).filter((project: any) => !project.folderID);
|
|
254
|
+
|
|
255
|
+
for (const project of rootProjects) {
|
|
256
|
+
output += processProject(project, 0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Process tasks in the Inbox (not in any project)
|
|
260
|
+
const inboxTasks = database.tasks.filter(function (task: any) {
|
|
261
|
+
return !task.projectId;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (inboxTasks.length > 0) {
|
|
265
|
+
output += `\nP: Inbox\n`;
|
|
266
|
+
for (const task of inboxTasks) {
|
|
267
|
+
output += processTask(task, 0);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return output;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Compute minimum unique prefixes for all tags (minimum 3 characters)
|
|
275
|
+
function computeMinimumUniquePrefixes(tagNames: string[]): Map<string, string> {
|
|
276
|
+
const prefixMap = new Map<string, string>();
|
|
277
|
+
|
|
278
|
+
// For each tag name
|
|
279
|
+
for (const tagName of tagNames) {
|
|
280
|
+
// Start with minimum length of 3
|
|
281
|
+
let prefixLength = 3;
|
|
282
|
+
let isUnique = false;
|
|
283
|
+
|
|
284
|
+
// Keep increasing prefix length until we find a unique prefix
|
|
285
|
+
while (!isUnique && prefixLength <= tagName.length) {
|
|
286
|
+
const prefix = tagName.substring(0, prefixLength);
|
|
287
|
+
|
|
288
|
+
// Check if this prefix uniquely identifies the tag
|
|
289
|
+
isUnique = tagNames.every(otherTag => {
|
|
290
|
+
// If it's the same tag, skip comparison
|
|
291
|
+
if (otherTag === tagName) return true;
|
|
292
|
+
|
|
293
|
+
// If the other tag starts with the same prefix, it's not unique
|
|
294
|
+
return !otherTag.startsWith(prefix);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (isUnique) {
|
|
298
|
+
prefixMap.set(tagName, prefix);
|
|
299
|
+
} else {
|
|
300
|
+
prefixLength++;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// If we couldn't find a unique prefix, use the full tag name
|
|
305
|
+
if (!isUnique) {
|
|
306
|
+
prefixMap.set(tagName, tagName);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return prefixMap;
|
|
311
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { editItem, EditItemParams } from '../primitives/editItem.js';
|
|
3
|
+
import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
4
|
+
|
|
5
|
+
export const schema = z.object({
|
|
6
|
+
id: z.string().optional().describe("The ID of the task or project to edit"),
|
|
7
|
+
name: z.string().optional().describe("The name of the task or project to edit (as fallback if ID not provided)"),
|
|
8
|
+
itemType: z.enum(['task', 'project']).describe("Type of item to edit ('task' or 'project')"),
|
|
9
|
+
|
|
10
|
+
// Common editable fields
|
|
11
|
+
newName: z.string().optional().describe("New name for the item"),
|
|
12
|
+
newNote: z.string().optional().describe("New note for the item"),
|
|
13
|
+
newDueDate: z.string().optional().describe("New due date in ISO format (YYYY-MM-DD or full ISO date); set to empty string to clear"),
|
|
14
|
+
newDeferDate: z.string().optional().describe("New defer date in ISO format (YYYY-MM-DD or full ISO date); set to empty string to clear"),
|
|
15
|
+
newFlagged: z.boolean().optional().describe("Set flagged status (set to false for no flag, true for flag)"),
|
|
16
|
+
newEstimatedMinutes: z.number().optional().describe("New estimated minutes"),
|
|
17
|
+
|
|
18
|
+
// Task-specific fields
|
|
19
|
+
newStatus: z.enum(['incomplete', 'completed', 'dropped']).optional().describe("New status for tasks (incomplete, completed, dropped)"),
|
|
20
|
+
addTags: z.array(z.string()).optional().describe("Tags to add to the task"),
|
|
21
|
+
removeTags: z.array(z.string()).optional().describe("Tags to remove from the task"),
|
|
22
|
+
replaceTags: z.array(z.string()).optional().describe("Tags to replace all existing tags with"),
|
|
23
|
+
|
|
24
|
+
// Project-specific fields
|
|
25
|
+
newSequential: z.boolean().optional().describe("Whether the project should be sequential"),
|
|
26
|
+
newFolderName: z.string().optional().describe("New folder to move the project to"),
|
|
27
|
+
newProjectStatus: z.enum(['active', 'completed', 'dropped', 'onHold']).optional().describe("New status for projects")
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export async function handler(args: z.infer<typeof schema>, extra: RequestHandlerExtra) {
|
|
31
|
+
try {
|
|
32
|
+
// Validate that either id or name is provided
|
|
33
|
+
if (!args.id && !args.name) {
|
|
34
|
+
return {
|
|
35
|
+
content: [{
|
|
36
|
+
type: "text" as const,
|
|
37
|
+
text: "Either id or name must be provided to edit an item."
|
|
38
|
+
}],
|
|
39
|
+
isError: true
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Call the editItem function
|
|
44
|
+
const result = await editItem(args as EditItemParams);
|
|
45
|
+
|
|
46
|
+
if (result.success) {
|
|
47
|
+
// Item was edited successfully
|
|
48
|
+
const itemTypeLabel = args.itemType === 'task' ? 'Task' : 'Project';
|
|
49
|
+
let changedText = '';
|
|
50
|
+
|
|
51
|
+
if (result.changedProperties) {
|
|
52
|
+
changedText = ` (${result.changedProperties})`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
content: [{
|
|
57
|
+
type: "text" as const,
|
|
58
|
+
text: `✅ ${itemTypeLabel} "${result.name}" updated successfully${changedText}.`
|
|
59
|
+
}]
|
|
60
|
+
};
|
|
61
|
+
} else {
|
|
62
|
+
// Item editing failed
|
|
63
|
+
let errorMsg = `Failed to update ${args.itemType}`;
|
|
64
|
+
|
|
65
|
+
if (result.error) {
|
|
66
|
+
if (result.error.includes("Item not found")) {
|
|
67
|
+
errorMsg = `${args.itemType.charAt(0).toUpperCase() + args.itemType.slice(1)} not found`;
|
|
68
|
+
if (args.id) errorMsg += ` with ID "${args.id}"`;
|
|
69
|
+
if (args.name) errorMsg += `${args.id ? ' or' : ' with'} name "${args.name}"`;
|
|
70
|
+
errorMsg += '.';
|
|
71
|
+
} else {
|
|
72
|
+
errorMsg += `: ${result.error}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
content: [{
|
|
78
|
+
type: "text" as const,
|
|
79
|
+
text: errorMsg
|
|
80
|
+
}],
|
|
81
|
+
isError: true
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
} catch (err: unknown) {
|
|
85
|
+
const error = err as Error;
|
|
86
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
content: [{
|
|
90
|
+
type: "text" as const,
|
|
91
|
+
text: `Error updating ${args.itemType}: ${error.message}`
|
|
92
|
+
}],
|
|
93
|
+
isError: true
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getPerspectiveView } from '../primitives/getPerspectiveView.js';
|
|
3
|
+
import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
4
|
+
|
|
5
|
+
export const schema = z.object({
|
|
6
|
+
perspectiveName: z.string().describe("Name of the perspective to view (e.g., 'Inbox', 'Projects', 'Flagged', or custom perspective name)"),
|
|
7
|
+
|
|
8
|
+
limit: z.number().optional().describe("Maximum number of items to return. Default: 100"),
|
|
9
|
+
|
|
10
|
+
includeMetadata: z.boolean().optional().describe("Include additional metadata like project names, tags, dates. Default: true"),
|
|
11
|
+
|
|
12
|
+
fields: z.array(z.string()).optional().describe("Specific fields to include in the response. Reduces response size. Available fields: id, name, note, flagged, dueDate, deferDate, completionDate, taskStatus, projectName, tagNames, estimatedMinutes")
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export async function handler(args: z.infer<typeof schema>, extra: RequestHandlerExtra) {
|
|
16
|
+
try {
|
|
17
|
+
const result = await getPerspectiveView({
|
|
18
|
+
perspectiveName: args.perspectiveName,
|
|
19
|
+
limit: args.limit ?? 100,
|
|
20
|
+
includeMetadata: args.includeMetadata ?? true,
|
|
21
|
+
fields: args.fields
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (result.success) {
|
|
25
|
+
const items = result.items || [];
|
|
26
|
+
|
|
27
|
+
// Format the output
|
|
28
|
+
let output = `## ${args.perspectiveName} Perspective (${items.length} items)\n\n`;
|
|
29
|
+
|
|
30
|
+
if (items.length === 0) {
|
|
31
|
+
output += "No items visible in this perspective.";
|
|
32
|
+
} else {
|
|
33
|
+
// Format each item
|
|
34
|
+
items.forEach(item => {
|
|
35
|
+
const parts = [];
|
|
36
|
+
|
|
37
|
+
// Core display
|
|
38
|
+
const flag = item.flagged ? '🚩 ' : '';
|
|
39
|
+
const checkbox = item.completed ? '☑' : '☐';
|
|
40
|
+
parts.push(`${checkbox} ${flag}${item.name || 'Unnamed'}`);
|
|
41
|
+
|
|
42
|
+
// Project context
|
|
43
|
+
if (item.projectName) {
|
|
44
|
+
parts.push(`(${item.projectName})`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Due date
|
|
48
|
+
if (item.dueDate) {
|
|
49
|
+
const date = new Date(item.dueDate);
|
|
50
|
+
const dateStr = `${date.getMonth() + 1}/${date.getDate()}`;
|
|
51
|
+
parts.push(`[due: ${dateStr}]`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Defer date
|
|
55
|
+
if (item.deferDate) {
|
|
56
|
+
const date = new Date(item.deferDate);
|
|
57
|
+
const dateStr = `${date.getMonth() + 1}/${date.getDate()}`;
|
|
58
|
+
parts.push(`[defer: ${dateStr}]`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Time estimate
|
|
62
|
+
if (item.estimatedMinutes) {
|
|
63
|
+
const hours = item.estimatedMinutes >= 60
|
|
64
|
+
? `${Math.floor(item.estimatedMinutes / 60)}h${item.estimatedMinutes % 60 > 0 ? (item.estimatedMinutes % 60) + 'm' : ''}`
|
|
65
|
+
: `${item.estimatedMinutes}m`;
|
|
66
|
+
parts.push(`(${hours})`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Tags
|
|
70
|
+
if (item.tagNames && item.tagNames.length > 0) {
|
|
71
|
+
parts.push(`<${item.tagNames.join(',')}>`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Status
|
|
75
|
+
if (item.taskStatus && item.taskStatus !== 'Available') {
|
|
76
|
+
parts.push(`#${item.taskStatus.toLowerCase()}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ID for reference
|
|
80
|
+
if (item.id) {
|
|
81
|
+
parts.push(`[${item.id}]`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
output += `• ${parts.join(' ')}\n`;
|
|
85
|
+
|
|
86
|
+
// Add note preview if present and not too long
|
|
87
|
+
if (item.note && item.note.trim()) {
|
|
88
|
+
const notePreview = item.note.trim().split('\n')[0].substring(0, 80);
|
|
89
|
+
const ellipsis = item.note.length > 80 || item.note.includes('\n') ? '...' : '';
|
|
90
|
+
output += ` └─ ${notePreview}${ellipsis}\n`;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (items.length === args.limit) {
|
|
96
|
+
output += `\n⚠️ Results limited to ${args.limit} items. More may be available in this perspective.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: "text" as const,
|
|
102
|
+
text: output
|
|
103
|
+
}]
|
|
104
|
+
};
|
|
105
|
+
} else {
|
|
106
|
+
return {
|
|
107
|
+
content: [{
|
|
108
|
+
type: "text" as const,
|
|
109
|
+
text: `Failed to get perspective view: ${result.error}`
|
|
110
|
+
}],
|
|
111
|
+
isError: true
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
} catch (err: unknown) {
|
|
115
|
+
const error = err as Error;
|
|
116
|
+
console.error(`Error getting perspective view: ${error.message}`);
|
|
117
|
+
return {
|
|
118
|
+
content: [{
|
|
119
|
+
type: "text" as const,
|
|
120
|
+
text: `Error getting perspective view: ${error.message}`
|
|
121
|
+
}],
|
|
122
|
+
isError: true
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { listPerspectives } from '../primitives/listPerspectives.js';
|
|
3
|
+
import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
4
|
+
|
|
5
|
+
export const schema = z.object({
|
|
6
|
+
includeBuiltIn: z.boolean().optional().describe("Include built-in perspectives (Inbox, Projects, Tags, etc.). Default: true"),
|
|
7
|
+
includeCustom: z.boolean().optional().describe("Include custom perspectives (Pro feature). Default: true")
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export async function handler(args: z.infer<typeof schema>, extra: RequestHandlerExtra) {
|
|
11
|
+
try {
|
|
12
|
+
const result = await listPerspectives({
|
|
13
|
+
includeBuiltIn: args.includeBuiltIn ?? true,
|
|
14
|
+
includeCustom: args.includeCustom ?? true
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (result.success) {
|
|
18
|
+
const perspectives = result.perspectives || [];
|
|
19
|
+
|
|
20
|
+
// Format the perspectives in a readable way
|
|
21
|
+
let output = `## Available Perspectives (${perspectives.length})\n\n`;
|
|
22
|
+
|
|
23
|
+
// Group by type
|
|
24
|
+
const builtIn = perspectives.filter(p => p.type === 'builtin');
|
|
25
|
+
const custom = perspectives.filter(p => p.type === 'custom');
|
|
26
|
+
|
|
27
|
+
if (builtIn.length > 0) {
|
|
28
|
+
output += `### Built-in Perspectives\n`;
|
|
29
|
+
builtIn.forEach(p => {
|
|
30
|
+
output += `• ${p.name}\n`;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (custom.length > 0) {
|
|
35
|
+
if (builtIn.length > 0) output += '\n';
|
|
36
|
+
output += `### Custom Perspectives\n`;
|
|
37
|
+
custom.forEach(p => {
|
|
38
|
+
output += `• ${p.name}\n`;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (perspectives.length === 0) {
|
|
43
|
+
output = "No perspectives found.";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
content: [{
|
|
48
|
+
type: "text" as const,
|
|
49
|
+
text: output
|
|
50
|
+
}]
|
|
51
|
+
};
|
|
52
|
+
} else {
|
|
53
|
+
return {
|
|
54
|
+
content: [{
|
|
55
|
+
type: "text" as const,
|
|
56
|
+
text: `Failed to list perspectives: ${result.error}`
|
|
57
|
+
}],
|
|
58
|
+
isError: true
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
} catch (err: unknown) {
|
|
62
|
+
const error = err as Error;
|
|
63
|
+
console.error(`Error listing perspectives: ${error.message}`);
|
|
64
|
+
return {
|
|
65
|
+
content: [{
|
|
66
|
+
type: "text" as const,
|
|
67
|
+
text: `Error listing perspectives: ${error.message}`
|
|
68
|
+
}],
|
|
69
|
+
isError: true
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|