omnifocus-mcp
Version:
Model Context Protocol (MCP) server that integrates with OmniFocus for AI assistant interaction
95 lines (94 loc) • 4.62 kB
JavaScript
import { z } from 'zod';
import { addOmniFocusTask } from '../primitives/addOmniFocusTask.js';
import { repeatShape } from './repeatSchema.js';
export const schema = z.object({
name: z.string().describe("Task name"),
note: z.string().optional().describe("Task note"),
dueDate: z.string().optional().describe("Due date (YYYY-MM-DD or full ISO)"),
deferDate: z.string().optional().describe("Defer date (YYYY-MM-DD or full ISO)"),
plannedDate: z.string().optional().describe("Planned date — the day you intend to work on it (YYYY-MM-DD or full ISO)"),
flagged: z.boolean().optional().describe("Flag the task"),
estimatedMinutes: z.number().optional().describe("Time estimate in minutes"),
tags: z.array(z.string()).optional().describe("Tag names to assign"),
projectId: z.string().optional().describe("Project id to place the task in; takes precedence over projectName. Prefer when names are ambiguous"),
projectName: z.string().optional().describe("Project name or folder path ('Work/My Project') to place the task in; defaults to inbox. To move an EXISTING task, use edit_item with newProjectName instead of creating a duplicate here"),
// Hierarchy support
parentTaskId: z.string().optional().describe("Parent task id (preferred over name)"),
parentTaskName: z.string().optional().describe("Parent task name; matched within the project if one is given"),
hierarchyLevel: z.number().int().min(0).optional().describe("Ordering hint for batch workflows (0 = root); ignored in single add"),
repeat: repeatShape.optional()
});
export async function handler(args, extra) {
try {
// Call the addOmniFocusTask function
const result = await addOmniFocusTask(args);
console.error('[add_omnifocus_task] args:', JSON.stringify(args));
console.error('[add_omnifocus_task] result:', JSON.stringify(result));
if (result.success) {
// Determine actual placement
const placement = result.placement;
let locationText = '';
if (placement === 'parent') {
locationText = 'under the parent task';
}
else if (placement === 'project') {
// Name the project we were asked for. "in a project" was the old
// fallback for the projectId path, which is exactly the caller who
// cannot check the result by eye.
if (args.projectName) {
locationText = `in project "${args.projectName}"`;
}
else if (args.projectId) {
locationText = `in project id ${args.projectId}`;
}
else {
locationText = 'in a project';
}
}
else {
locationText = 'in your inbox';
}
const tagText = args.tags && args.tags.length > 0
? ` with tags: ${args.tags.join(', ')}`
: '';
const dueDateText = args.dueDate
? ` due on ${new Date(args.dueDate).toLocaleDateString()}`
: '';
// Warning if parent requested but not used
let placementWarning = '';
if ((args.parentTaskId || args.parentTaskName) && placement && placement !== 'parent') {
placementWarning = `\n⚠️ Parent not found; task created ${placement === 'project' ? 'in project' : 'in inbox'}.`;
}
// Echo the id (#104): without it, agents re-query immediately after every
// write just to harvest the id for the follow-up edit.
const idText = result.taskId ? ` (id: ${result.taskId})` : '';
return {
content: [{
type: "text",
text: `✅ Task "${args.name}" created successfully ${locationText}${dueDateText}${tagText}${idText}.${placementWarning}`
}]
};
}
else {
// Task creation failed
return {
content: [{
type: "text",
text: `Failed to create task: ${result.error}`
}],
isError: true
};
}
}
catch (err) {
const error = err;
console.error(`Tool execution error: ${error.message}`);
return {
content: [{
type: "text",
text: `Error creating task: ${error.message}`
}],
isError: true
};
}
}