@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,113 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { writeFileSync, unlinkSync, readFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { tmpdir } from 'os';
6
+ import { fileURLToPath } from 'url';
7
+ import { dirname } from 'path';
8
+ import { existsSync } from 'fs';
9
+ const execAsync = promisify(exec);
10
+ // Helper function to execute OmniFocus scripts
11
+ export async function executeJXA(script) {
12
+ try {
13
+ // Write the script to a temporary file in the system temp directory
14
+ const tempFile = join(tmpdir(), `jxa_script_${Date.now()}.js`);
15
+ // Write the script to the temporary file
16
+ writeFileSync(tempFile, script);
17
+ // Execute the script using osascript
18
+ const { stdout, stderr } = await execAsync(`osascript -l JavaScript ${tempFile}`);
19
+ if (stderr) {
20
+ console.error("Script stderr output:", stderr);
21
+ }
22
+ // Clean up the temporary file
23
+ unlinkSync(tempFile);
24
+ // Parse the output as JSON
25
+ try {
26
+ const result = JSON.parse(stdout);
27
+ return result;
28
+ }
29
+ catch (e) {
30
+ console.error("Failed to parse script output as JSON:", e);
31
+ // If this contains a "Found X tasks" message, treat it as a successful non-JSON response
32
+ if (stdout.includes("Found") && stdout.includes("tasks")) {
33
+ return [];
34
+ }
35
+ return [];
36
+ }
37
+ }
38
+ catch (error) {
39
+ console.error("Failed to execute JXA script:", error);
40
+ throw error;
41
+ }
42
+ }
43
+ // Function to execute scripts in OmniFocus using the URL scheme
44
+ // Update src/utils/scriptExecution.ts
45
+ export async function executeOmniFocusScript(scriptPath, args) {
46
+ try {
47
+ // Get the actual script path (existing code remains the same)
48
+ let actualPath;
49
+ if (scriptPath.startsWith('@')) {
50
+ const scriptName = scriptPath.substring(1);
51
+ const __filename = fileURLToPath(import.meta.url);
52
+ const __dirname = dirname(__filename);
53
+ const distPath = join(__dirname, '..', 'utils', 'omnifocusScripts', scriptName);
54
+ const srcPath = join(__dirname, '..', '..', 'src', 'utils', 'omnifocusScripts', scriptName);
55
+ if (existsSync(distPath)) {
56
+ actualPath = distPath;
57
+ }
58
+ else if (existsSync(srcPath)) {
59
+ actualPath = srcPath;
60
+ }
61
+ else {
62
+ actualPath = join(__dirname, '..', 'omnifocusScripts', scriptName);
63
+ }
64
+ }
65
+ else {
66
+ actualPath = scriptPath;
67
+ }
68
+ // Read the script file
69
+ const scriptContent = readFileSync(actualPath, 'utf8');
70
+ // Create a temporary file for our JXA wrapper script
71
+ const tempFile = join(tmpdir(), `jxa_wrapper_${Date.now()}.js`);
72
+ // Escape the script content properly for use in JXA
73
+ const escapedScript = scriptContent.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
74
+ // Create a JXA script that will execute our OmniJS script in OmniFocus
75
+ const jxaScript = `
76
+ function run() {
77
+ try {
78
+ const app = Application('OmniFocus');
79
+ app.includeStandardAdditions = true;
80
+
81
+ // Run the OmniJS script in OmniFocus and capture the output
82
+ const result = app.evaluateJavascript(\`${escapedScript}\`);
83
+
84
+ // Return the result
85
+ return result;
86
+ } catch (e) {
87
+ return JSON.stringify({ error: e.message });
88
+ }
89
+ }
90
+ `;
91
+ // Write the JXA script to the temporary file
92
+ writeFileSync(tempFile, jxaScript);
93
+ // Execute the JXA script using osascript
94
+ const { stdout, stderr } = await execAsync(`osascript -l JavaScript ${tempFile}`);
95
+ // Clean up the temporary file
96
+ unlinkSync(tempFile);
97
+ if (stderr) {
98
+ console.error("Script stderr output:", stderr);
99
+ }
100
+ // Parse the output as JSON
101
+ try {
102
+ return JSON.parse(stdout);
103
+ }
104
+ catch (parseError) {
105
+ console.error("Error parsing script output:", parseError);
106
+ return stdout;
107
+ }
108
+ }
109
+ catch (error) {
110
+ console.error("Failed to execute OmniFocus script:", error);
111
+ throw error;
112
+ }
113
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@iflow-mcp/omnifocus-mcp",
3
+ "type": "module",
4
+ "version": "1.2.3",
5
+ "description": "Model Context Protocol (MCP) server that integrates with OmniFocus for AI assistant interaction",
6
+ "main": "dist/server.js",
7
+ "bin": {
8
+ "omnifocus-mcp": "./cli.cjs"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc && npm run copy-files && chmod 755 dist/server.js",
12
+ "copy-files": "mkdir -p dist/utils/omnifocusScripts && cp src/utils/omnifocusScripts/*.js dist/utils/omnifocusScripts/",
13
+ "start": "node dist/server.js",
14
+ "dev": "tsc -w"
15
+ },
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.8.0",
18
+ "zod": "^3.22.4"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^20.11.19",
22
+ "typescript": "^5.3.3"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/themotionmachine/omnifocus-mcp-server.git"
27
+ },
28
+ "homepage": "https://github.com/themotionmachine/omnifocus-mcp-server",
29
+ "keywords": [
30
+ "omnifocus",
31
+ "mcp",
32
+ "claude",
33
+ "task-management",
34
+ "ai"
35
+ ],
36
+ "license": "MIT"
37
+ }
@@ -0,0 +1,89 @@
1
+ // Core enums
2
+ export namespace Task {
3
+ export enum Status {
4
+ Available,
5
+ Blocked,
6
+ Completed,
7
+ Dropped,
8
+ DueSoon,
9
+ Next,
10
+ Overdue
11
+ }
12
+
13
+ export enum RepetitionMethod {
14
+ DeferUntilDate,
15
+ DueDate,
16
+ Fixed,
17
+ None
18
+ }
19
+ }
20
+ export namespace Project {
21
+ export enum Status {
22
+ Active,
23
+ Done,
24
+ Dropped,
25
+ OnHold
26
+ }
27
+ }
28
+ export namespace Folder {
29
+ export enum Status {
30
+ Active,
31
+ Dropped
32
+ }
33
+ }
34
+ export namespace Tag {
35
+ export enum Status {
36
+ Active,
37
+ Dropped,
38
+ OnHold
39
+ }
40
+ }
41
+ // Minimal DatabaseObject interface
42
+ export interface DatabaseObject {
43
+ id: { primaryKey: string };
44
+ }
45
+ // Minimal interfaces for core objects
46
+ export interface TaskMinimal extends DatabaseObject {
47
+ name: string;
48
+ note: string;
49
+ flagged: boolean;
50
+ taskStatus: Task.Status;
51
+ dueDate: Date | null;
52
+ deferDate: Date | null;
53
+ effectiveDueDate: Date | null;
54
+ effectiveDeferDate: Date | null;
55
+ estimatedMinutes: number | null;
56
+ completedByChildren: boolean;
57
+ sequential: boolean;
58
+ tags: TagMinimal[];
59
+ containingProject: ProjectMinimal | null;
60
+ parent: TaskMinimal | null;
61
+ children: TaskMinimal[];
62
+ inInbox: boolean;
63
+ }
64
+ export interface ProjectMinimal extends DatabaseObject {
65
+ name: string;
66
+ note: string;
67
+ status: Project.Status;
68
+ dueDate: Date | null;
69
+ deferDate: Date | null;
70
+ effectiveDueDate: Date | null;
71
+ effectiveDeferDate: Date | null;
72
+ estimatedMinutes: number | null;
73
+ flagged: boolean;
74
+ sequential: boolean;
75
+ parentFolder: FolderMinimal | null;
76
+ tags: TagMinimal[];
77
+ tasks: TaskMinimal[];
78
+ }
79
+ export interface FolderMinimal extends DatabaseObject {
80
+ name: string;
81
+ status: Folder.Status;
82
+ parent: FolderMinimal | null;
83
+ }
84
+ export interface TagMinimal extends DatabaseObject {
85
+ name: string;
86
+ status: Tag.Status;
87
+ parent: TagMinimal | null;
88
+ active: boolean;
89
+ }
package/src/server.ts ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // Import tool definitions
7
+ import * as dumpDatabaseTool from './tools/definitions/dumpDatabase.js';
8
+ import * as addOmniFocusTaskTool from './tools/definitions/addOmniFocusTask.js';
9
+ import * as addProjectTool from './tools/definitions/addProject.js';
10
+ import * as removeItemTool from './tools/definitions/removeItem.js';
11
+ import * as editItemTool from './tools/definitions/editItem.js';
12
+ import * as batchAddItemsTool from './tools/definitions/batchAddItems.js';
13
+ import * as batchRemoveItemsTool from './tools/definitions/batchRemoveItems.js';
14
+ import * as queryOmniFocusTool from './tools/definitions/queryOmnifocus.js';
15
+ import * as listPerspectivesTool from './tools/definitions/listPerspectives.js';
16
+ import * as getPerspectiveViewTool from './tools/definitions/getPerspectiveView.js';
17
+
18
+ // Create an MCP server
19
+ const server = new McpServer({
20
+ name: "OmniFocus MCP",
21
+ version: "1.0.0"
22
+ });
23
+
24
+ // Register tools
25
+ server.tool(
26
+ "dump_database",
27
+ "Gets the current state of your OmniFocus database",
28
+ dumpDatabaseTool.schema.shape,
29
+ dumpDatabaseTool.handler
30
+ );
31
+
32
+ server.tool(
33
+ "add_omnifocus_task",
34
+ "Add a new task to OmniFocus",
35
+ addOmniFocusTaskTool.schema.shape,
36
+ addOmniFocusTaskTool.handler
37
+ );
38
+
39
+ server.tool(
40
+ "add_project",
41
+ "Add a new project to OmniFocus",
42
+ addProjectTool.schema.shape,
43
+ addProjectTool.handler
44
+ );
45
+
46
+ server.tool(
47
+ "remove_item",
48
+ "Remove a task or project from OmniFocus",
49
+ removeItemTool.schema.shape,
50
+ removeItemTool.handler
51
+ );
52
+
53
+ server.tool(
54
+ "edit_item",
55
+ "Edit a task or project in OmniFocus",
56
+ editItemTool.schema.shape,
57
+ editItemTool.handler
58
+ );
59
+
60
+ server.tool(
61
+ "batch_add_items",
62
+ "Add multiple tasks or projects to OmniFocus in a single operation",
63
+ batchAddItemsTool.schema.shape,
64
+ batchAddItemsTool.handler
65
+ );
66
+
67
+ server.tool(
68
+ "batch_remove_items",
69
+ "Remove multiple tasks or projects from OmniFocus in a single operation",
70
+ batchRemoveItemsTool.schema.shape,
71
+ batchRemoveItemsTool.handler
72
+ );
73
+
74
+ server.tool(
75
+ "query_omnifocus",
76
+ "Efficiently query OmniFocus database with powerful filters. Get specific tasks, projects, or folders without loading the entire database. Supports filtering by project, tags, status, due dates, and more. Much faster than dump_database for targeted queries.",
77
+ queryOmniFocusTool.schema.shape,
78
+ queryOmniFocusTool.handler
79
+ );
80
+
81
+ server.tool(
82
+ "list_perspectives",
83
+ "List all available perspectives in OmniFocus, including built-in perspectives (Inbox, Projects, Tags, etc.) and custom perspectives (Pro feature)",
84
+ listPerspectivesTool.schema.shape,
85
+ listPerspectivesTool.handler
86
+ );
87
+
88
+ server.tool(
89
+ "get_perspective_view",
90
+ "Get the items visible in a specific OmniFocus perspective. Shows what tasks and projects are displayed when viewing that perspective",
91
+ getPerspectiveViewTool.schema.shape,
92
+ getPerspectiveViewTool.handler
93
+ );
94
+
95
+ // Start the MCP server
96
+ const transport = new StdioServerTransport();
97
+
98
+ // Use await with server.connect to ensure proper connection
99
+ (async function() {
100
+ try {
101
+ console.error("Starting MCP server...");
102
+ await server.connect(transport);
103
+ console.error("MCP Server connected and ready to accept commands from Claude");
104
+ } catch (err) {
105
+ console.error(`Failed to start MCP server: ${err}`);
106
+ }
107
+ })();
108
+
109
+ // For a cleaner shutdown if the process is terminated
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import { addOmniFocusTask, AddOmniFocusTaskParams } from '../primitives/addOmniFocusTask.js';
3
+ import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
4
+
5
+ export const schema = z.object({
6
+ name: z.string().describe("The name of the task"),
7
+ note: z.string().optional().describe("Additional notes for the task"),
8
+ dueDate: z.string().optional().describe("The due date of the task in ISO format (YYYY-MM-DD or full ISO date)"),
9
+ deferDate: z.string().optional().describe("The defer date of the task in ISO format (YYYY-MM-DD or full ISO date)"),
10
+ flagged: z.boolean().optional().describe("Whether the task is flagged or not"),
11
+ estimatedMinutes: z.number().optional().describe("Estimated time to complete the task, in minutes"),
12
+ tags: z.array(z.string()).optional().describe("Tags to assign to the task"),
13
+ projectName: z.string().optional().describe("The name of the project to add the task to (will add to inbox if not specified)"),
14
+ // Hierarchy support
15
+ parentTaskId: z.string().optional().describe("ID of the parent task (preferred for accuracy)"),
16
+ parentTaskName: z.string().optional().describe("Name of the parent task (used if ID not provided; matched within project or globally if no project)"),
17
+ hierarchyLevel: z.number().int().min(0).optional().describe("Explicit level indicator for ordering in batch workflows (0=root) - ignored in single add")
18
+ });
19
+
20
+ export async function handler(args: z.infer<typeof schema>, extra: RequestHandlerExtra) {
21
+ try {
22
+ // Call the addOmniFocusTask function
23
+ const result = await addOmniFocusTask(args as AddOmniFocusTaskParams);
24
+ console.error('[add_omnifocus_task] args:', JSON.stringify(args));
25
+ console.error('[add_omnifocus_task] result:', JSON.stringify(result));
26
+
27
+ if (result.success) {
28
+ // Determine actual placement
29
+ const placement = (result as any).placement as 'parent' | 'project' | 'inbox' | undefined;
30
+ let locationText = '';
31
+ if (placement === 'parent') {
32
+ locationText = 'under the parent task';
33
+ } else if (placement === 'project') {
34
+ locationText = args.projectName ? `in project "${args.projectName}"` : 'in a project';
35
+ } else {
36
+ locationText = 'in your inbox';
37
+ }
38
+
39
+ const tagText = args.tags && args.tags.length > 0
40
+ ? ` with tags: ${args.tags.join(', ')}`
41
+ : '';
42
+
43
+ const dueDateText = args.dueDate
44
+ ? ` due on ${new Date(args.dueDate).toLocaleDateString()}`
45
+ : '';
46
+
47
+ // Warning if parent requested but not used
48
+ let placementWarning = '';
49
+ if ((args.parentTaskId || args.parentTaskName) && placement && placement !== 'parent') {
50
+ placementWarning = `\n⚠️ Parent not found; task created ${placement === 'project' ? 'in project' : 'in inbox'}.`;
51
+ }
52
+
53
+ return {
54
+ content: [{
55
+ type: "text" as const,
56
+ text: `✅ Task "${args.name}" created successfully ${locationText}${dueDateText}${tagText}.${placementWarning}`
57
+ }]
58
+ };
59
+ } else {
60
+ // Task creation failed
61
+ return {
62
+ content: [{
63
+ type: "text" as const,
64
+ text: `Failed to create task: ${result.error}`
65
+ }],
66
+ isError: true
67
+ };
68
+ }
69
+ } catch (err: unknown) {
70
+ const error = err as Error;
71
+ console.error(`Tool execution error: ${error.message}`);
72
+ return {
73
+ content: [{
74
+ type: "text" as const,
75
+ text: `Error creating task: ${error.message}`
76
+ }],
77
+ isError: true
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,67 @@
1
+ import { z } from 'zod';
2
+ import { addProject, AddProjectParams } from '../primitives/addProject.js';
3
+ import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
4
+
5
+ export const schema = z.object({
6
+ name: z.string().describe("The name of the project"),
7
+ note: z.string().optional().describe("Additional notes for the project"),
8
+ dueDate: z.string().optional().describe("The due date of the project in ISO format (YYYY-MM-DD or full ISO date)"),
9
+ deferDate: z.string().optional().describe("The defer date of the project in ISO format (YYYY-MM-DD or full ISO date)"),
10
+ flagged: z.boolean().optional().describe("Whether the project is flagged or not"),
11
+ estimatedMinutes: z.number().optional().describe("Estimated time to complete the project, in minutes"),
12
+ tags: z.array(z.string()).optional().describe("Tags to assign to the project"),
13
+ folderName: z.string().optional().describe("The name of the folder to add the project to (will add to root if not specified)"),
14
+ sequential: z.boolean().optional().describe("Whether tasks in the project should be sequential (default: false)")
15
+ });
16
+
17
+ export async function handler(args: z.infer<typeof schema>, extra: RequestHandlerExtra) {
18
+ try {
19
+ // Call the addProject function
20
+ const result = await addProject(args as AddProjectParams);
21
+
22
+ if (result.success) {
23
+ // Project was added successfully
24
+ let locationText = args.folderName
25
+ ? `in folder "${args.folderName}"`
26
+ : "at the root level";
27
+
28
+ let tagText = args.tags && args.tags.length > 0
29
+ ? ` with tags: ${args.tags.join(', ')}`
30
+ : "";
31
+
32
+ let dueDateText = args.dueDate
33
+ ? ` due on ${new Date(args.dueDate).toLocaleDateString()}`
34
+ : "";
35
+
36
+ let sequentialText = args.sequential
37
+ ? " (sequential)"
38
+ : " (parallel)";
39
+
40
+ return {
41
+ content: [{
42
+ type: "text" as const,
43
+ text: `✅ Project "${args.name}" created successfully ${locationText}${dueDateText}${tagText}${sequentialText}.`
44
+ }]
45
+ };
46
+ } else {
47
+ // Project creation failed
48
+ return {
49
+ content: [{
50
+ type: "text" as const,
51
+ text: `Failed to create project: ${result.error}`
52
+ }],
53
+ isError: true
54
+ };
55
+ }
56
+ } catch (err: unknown) {
57
+ const error = err as Error;
58
+ console.error(`Tool execution error: ${error.message}`);
59
+ return {
60
+ content: [{
61
+ type: "text" as const,
62
+ text: `Error creating project: ${error.message}`
63
+ }],
64
+ isError: true
65
+ };
66
+ }
67
+ }
@@ -0,0 +1,98 @@
1
+ import { z } from 'zod';
2
+ import { batchAddItems, BatchAddItemsParams } from '../primitives/batchAddItems.js';
3
+ import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
4
+
5
+ export const schema = z.object({
6
+ items: z.array(z.object({
7
+ type: z.enum(['task', 'project']).describe("Type of item to add ('task' or 'project')"),
8
+ name: z.string().describe("The name of the item"),
9
+ note: z.string().optional().describe("Additional notes for the item"),
10
+ dueDate: z.string().optional().describe("The due date in ISO format (YYYY-MM-DD or full ISO date)"),
11
+ deferDate: z.string().optional().describe("The defer date in ISO format (YYYY-MM-DD or full ISO date)"),
12
+ flagged: z.boolean().optional().describe("Whether the item is flagged or not"),
13
+ estimatedMinutes: z.number().optional().describe("Estimated time to complete the item, in minutes"),
14
+ tags: z.array(z.string()).optional().describe("Tags to assign to the item"),
15
+
16
+ // Task-specific properties
17
+ projectName: z.string().optional().describe("For tasks: The name of the project to add the task to"),
18
+ parentTaskId: z.string().optional().describe("For tasks: ID of the parent task"),
19
+ parentTaskName: z.string().optional().describe("For tasks: Name of the parent task (scoped to project when provided)"),
20
+ tempId: z.string().optional().describe("For tasks: Temporary ID for within-batch references"),
21
+ parentTempId: z.string().optional().describe("For tasks: Reference to parent's tempId within the batch"),
22
+ hierarchyLevel: z.number().int().min(0).optional().describe("Optional ordering hint (0=root, 1=child, ...)"),
23
+
24
+ // Project-specific properties
25
+ folderName: z.string().optional().describe("For projects: The name of the folder to add the project to"),
26
+ sequential: z.boolean().optional().describe("For projects: Whether tasks in the project should be sequential")
27
+ })).describe("Array of items (tasks or projects) to add")
28
+ ,
29
+ createSequentially: z.boolean().optional().describe("Process parents before children; when false, best-effort order will still try to resolve parents first")
30
+ });
31
+
32
+ export async function handler(args: z.infer<typeof schema>, extra: RequestHandlerExtra) {
33
+ try {
34
+ // Call the batchAddItems function
35
+ const result = await batchAddItems(args.items as BatchAddItemsParams[]);
36
+
37
+ if (result.success) {
38
+ const successCount = result.results.filter(r => r.success).length;
39
+ const failureCount = result.results.filter(r => !r.success).length;
40
+
41
+ let message = `✅ Successfully added ${successCount} items.`;
42
+
43
+ if (failureCount > 0) {
44
+ message += ` ⚠️ Failed to add ${failureCount} items.`;
45
+ }
46
+
47
+ // Include details about added items
48
+ const details = result.results.map((item, index) => {
49
+ if (item.success) {
50
+ const itemType = args.items[index].type;
51
+ const itemName = args.items[index].name;
52
+ return `- ✅ ${itemType}: "${itemName}"`;
53
+ } else {
54
+ const itemType = args.items[index].type;
55
+ const itemName = args.items[index].name;
56
+ return `- ❌ ${itemType}: "${itemName}" - Error: ${item.error}`;
57
+ }
58
+ }).join('\n');
59
+
60
+ return {
61
+ content: [{
62
+ type: "text" as const,
63
+ text: `${message}\n\n${details}`
64
+ }]
65
+ };
66
+ } else {
67
+ console.error('[batch_add_items] failure result:', JSON.stringify(result));
68
+ // Batch operation failed completely or no items succeeded.
69
+ const failureDetails = (result.results && result.results.length > 0)
70
+ ? result.results.map((r, index) => {
71
+ const itemType = args.items[index].type;
72
+ const itemName = args.items[index].name;
73
+ return r.success
74
+ ? `- ✅ ${itemType}: \"${itemName}\"`
75
+ : `- ❌ ${itemType}: \"${itemName}\" - Error: ${r?.error || 'Unknown error'}`;
76
+ }).join('\\n')
77
+ : `No items processed. ${result.error || ''}`;
78
+
79
+ return {
80
+ content: [{
81
+ type: "text" as const,
82
+ text: `Failed to process batch operation.\\n\\n${failureDetails}`
83
+ }],
84
+ isError: true
85
+ };
86
+ }
87
+ } catch (err: unknown) {
88
+ const error = err as Error;
89
+ console.error(`Tool execution error: ${error.message}`);
90
+ return {
91
+ content: [{
92
+ type: "text" as const,
93
+ text: `Error processing batch operation: ${error.message}`
94
+ }],
95
+ isError: true
96
+ };
97
+ }
98
+ }
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import { batchRemoveItems, BatchRemoveItemsParams } from '../primitives/batchRemoveItems.js';
3
+ import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
4
+
5
+ export const schema = z.object({
6
+ items: z.array(z.object({
7
+ id: z.string().optional().describe("The ID of the task or project to remove"),
8
+ name: z.string().optional().describe("The name of the task or project to remove (as fallback if ID not provided)"),
9
+ itemType: z.enum(['task', 'project']).describe("Type of item to remove ('task' or 'project')")
10
+ })).describe("Array of items (tasks or projects) to remove")
11
+ });
12
+
13
+ export async function handler(args: z.infer<typeof schema>, extra: RequestHandlerExtra) {
14
+ try {
15
+ // Validate that each item has at least an ID or name
16
+ for (const item of args.items) {
17
+ if (!item.id && !item.name) {
18
+ return {
19
+ content: [{
20
+ type: "text" as const,
21
+ text: "Each item must have either id or name provided to remove it."
22
+ }],
23
+ isError: true
24
+ };
25
+ }
26
+ }
27
+
28
+ // Call the batchRemoveItems function
29
+ const result = await batchRemoveItems(args.items as BatchRemoveItemsParams[]);
30
+
31
+ if (result.success) {
32
+ const successCount = result.results.filter(r => r.success).length;
33
+ const failureCount = result.results.filter(r => !r.success).length;
34
+
35
+ let message = `✅ Successfully removed ${successCount} items.`;
36
+
37
+ if (failureCount > 0) {
38
+ message += ` ⚠️ Failed to remove ${failureCount} items.`;
39
+ }
40
+
41
+ // Include details about removed items
42
+ const details = result.results.map((item, index) => {
43
+ if (item.success) {
44
+ const itemType = args.items[index].itemType;
45
+ return `- ✅ ${itemType}: "${item.name}"`;
46
+ } else {
47
+ const itemType = args.items[index].itemType;
48
+ const identifier = args.items[index].id || args.items[index].name;
49
+ return `- ❌ ${itemType}: ${identifier} - Error: ${item.error}`;
50
+ }
51
+ }).join('\n');
52
+
53
+ return {
54
+ content: [{
55
+ type: "text" as const,
56
+ text: `${message}\n\n${details}`
57
+ }]
58
+ };
59
+ } else {
60
+ // Batch operation failed completely
61
+ return {
62
+ content: [{
63
+ type: "text" as const,
64
+ text: `Failed to process batch removal: ${result.error}`
65
+ }],
66
+ isError: true
67
+ };
68
+ }
69
+ } catch (err: unknown) {
70
+ const error = err as Error;
71
+ console.error(`Tool execution error: ${error.message}`);
72
+ return {
73
+ content: [{
74
+ type: "text" as const,
75
+ text: `Error processing batch removal: ${error.message}`
76
+ }],
77
+ isError: true
78
+ };
79
+ }
80
+ }