@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.
Files changed (69) hide show
  1. package/QUERY_TOOL_EXAMPLES.md +298 -0
  2. package/QUERY_TOOL_REFERENCE.md +228 -0
  3. package/README.md +250 -0
  4. package/assets/omnifocus-mcp-logo.png +0 -0
  5. package/cli.cjs +9 -0
  6. package/dist/omnifocustypes.js +48 -0
  7. package/dist/server.js +44 -0
  8. package/dist/tools/definitions/addOmniFocusTask.js +76 -0
  9. package/dist/tools/definitions/addProject.js +61 -0
  10. package/dist/tools/definitions/batchAddItems.js +89 -0
  11. package/dist/tools/definitions/batchRemoveItems.js +74 -0
  12. package/dist/tools/definitions/dumpDatabase.js +259 -0
  13. package/dist/tools/definitions/editItem.js +88 -0
  14. package/dist/tools/definitions/getPerspectiveView.js +107 -0
  15. package/dist/tools/definitions/listPerspectives.js +65 -0
  16. package/dist/tools/definitions/queryOmnifocus.js +190 -0
  17. package/dist/tools/definitions/removeItem.js +80 -0
  18. package/dist/tools/dumpDatabase.js +121 -0
  19. package/dist/tools/dumpDatabaseOptimized.js +192 -0
  20. package/dist/tools/primitives/addOmniFocusTask.js +227 -0
  21. package/dist/tools/primitives/addProject.js +132 -0
  22. package/dist/tools/primitives/batchAddItems.js +166 -0
  23. package/dist/tools/primitives/batchRemoveItems.js +44 -0
  24. package/dist/tools/primitives/editItem.js +443 -0
  25. package/dist/tools/primitives/getPerspectiveView.js +50 -0
  26. package/dist/tools/primitives/listPerspectives.js +34 -0
  27. package/dist/tools/primitives/queryOmnifocus.js +365 -0
  28. package/dist/tools/primitives/queryOmnifocusDebug.js +135 -0
  29. package/dist/tools/primitives/removeItem.js +177 -0
  30. package/dist/types.js +1 -0
  31. package/dist/utils/cacheManager.js +187 -0
  32. package/dist/utils/dateFormatting.js +58 -0
  33. package/dist/utils/omnifocusScripts/getPerspectiveView.js +169 -0
  34. package/dist/utils/omnifocusScripts/listPerspectives.js +59 -0
  35. package/dist/utils/omnifocusScripts/omnifocusDump.js +223 -0
  36. package/dist/utils/scriptExecution.js +113 -0
  37. package/package.json +37 -0
  38. package/src/omnifocustypes.ts +89 -0
  39. package/src/server.ts +109 -0
  40. package/src/tools/definitions/addOmniFocusTask.ts +80 -0
  41. package/src/tools/definitions/addProject.ts +67 -0
  42. package/src/tools/definitions/batchAddItems.ts +98 -0
  43. package/src/tools/definitions/batchRemoveItems.ts +80 -0
  44. package/src/tools/definitions/dumpDatabase.ts +311 -0
  45. package/src/tools/definitions/editItem.ts +96 -0
  46. package/src/tools/definitions/getPerspectiveView.ts +125 -0
  47. package/src/tools/definitions/listPerspectives.ts +72 -0
  48. package/src/tools/definitions/queryOmnifocus.ts +212 -0
  49. package/src/tools/definitions/removeItem.ts +86 -0
  50. package/src/tools/dumpDatabase.ts +196 -0
  51. package/src/tools/dumpDatabaseOptimized.ts +231 -0
  52. package/src/tools/primitives/addOmniFocusTask.ts +252 -0
  53. package/src/tools/primitives/addProject.ts +156 -0
  54. package/src/tools/primitives/batchAddItems.ts +207 -0
  55. package/src/tools/primitives/batchRemoveItems.ts +64 -0
  56. package/src/tools/primitives/editItem.ts +507 -0
  57. package/src/tools/primitives/getPerspectiveView.ts +71 -0
  58. package/src/tools/primitives/listPerspectives.ts +53 -0
  59. package/src/tools/primitives/queryOmnifocus.ts +394 -0
  60. package/src/tools/primitives/queryOmnifocusDebug.ts +139 -0
  61. package/src/tools/primitives/removeItem.ts +195 -0
  62. package/src/types.ts +107 -0
  63. package/src/utils/cacheManager.ts +234 -0
  64. package/src/utils/dateFormatting.ts +81 -0
  65. package/src/utils/omnifocusScripts/getPerspectiveView.js +169 -0
  66. package/src/utils/omnifocusScripts/listPerspectives.js +59 -0
  67. package/src/utils/omnifocusScripts/omnifocusDump.js +223 -0
  68. package/src/utils/scriptExecution.ts +128 -0
  69. 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
+ }