@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,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
|
+
}
|