Skip to main content
Glama

OmniFocus-MCP

dumpDatabase.ts9.24 kB
import { z } from 'zod'; import { dumpDatabase } from '../dumpDatabase.js'; import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; export const schema = z.object({ hideCompleted: z.boolean().optional().describe("Set to false to show completed and dropped tasks (default: true)"), hideRecurringDuplicates: z.boolean().optional().describe("Set to true to hide duplicate instances of recurring tasks (default: true)") }); export async function handler(args: z.infer<typeof schema>, extra: RequestHandlerExtra) { try { // Get raw database const database = await dumpDatabase(); // Format as compact report const formattedReport = formatCompactReport(database, { hideCompleted: args.hideCompleted !== false, // Default to true hideRecurringDuplicates: args.hideRecurringDuplicates !== false // Default to true }); return { content: [{ type: "text" as const, text: formattedReport }] }; } catch (err: unknown) { return { content: [{ type: "text" as const, text: `Error generating report. Please ensure OmniFocus is running and try again.` }], isError: true }; } } // Function to format date in compact format (M/D) function formatCompactDate(isoDate: string | null): string { if (!isoDate) return ''; const date = new Date(isoDate); return `${date.getMonth() + 1}/${date.getDate()}`; } // Function to format the database in the compact report format function formatCompactReport(database: any, options: { hideCompleted: boolean, hideRecurringDuplicates: boolean }): string { const { hideCompleted, hideRecurringDuplicates } = options; // Get current date for the header const today = new Date(); const dateStr = today.toISOString().split('T')[0]; let output = `# OMNIFOCUS [${dateStr}]\n\n`; // Add legend output += `FORMAT LEGEND: F: Folder | P: Project | •: Task | 🚩: Flagged Dates: [M/D] | Duration: (30m) or (2h) | Tags: <tag1,tag2> Status: #next #avail #block #due #over #compl #drop\n\n`; // Map of folder IDs to folder objects for quick lookup const folderMap = new Map(); Object.values(database.folders).forEach((folder: any) => { folderMap.set(folder.id, folder); }); // Get all tag names to compute minimum unique prefixes const allTagNames = Object.values(database.tags).map((tag: any) => tag.name); const tagPrefixMap = computeMinimumUniquePrefixes(allTagNames); // Function to get folder hierarchy path function getFolderPath(folderId: string): string[] { const path = []; let currentId = folderId; while (currentId) { const folder = folderMap.get(currentId); if (!folder) break; path.unshift(folder.name); currentId = folder.parentFolderID; } return path; } // Get root folders (no parent) const rootFolders = Object.values(database.folders).filter((folder: any) => !folder.parentFolderID); // Process folders recursively function processFolder(folder: any, level: number): string { const indent = ' '.repeat(level); let folderOutput = `${indent}F: ${folder.name}\n`; // Process subfolders if (folder.subfolders && folder.subfolders.length > 0) { for (const subfolderId of folder.subfolders) { const subfolder = database.folders[subfolderId]; if (subfolder) { folderOutput += `${processFolder(subfolder, level + 1)}`; } } } // Process projects in this folder if (folder.projects && folder.projects.length > 0) { for (const projectId of folder.projects) { const project = database.projects[projectId]; if (project) { folderOutput += processProject(project, level + 1); } } } return folderOutput; } // Process a project function processProject(project: any, level: number): string { const indent = ' '.repeat(level); // Skip if it's completed or dropped and we're hiding completed items if (hideCompleted && (project.status === 'Done' || project.status === 'Dropped')) { return ''; } // Format project status info let statusInfo = ''; if (project.status === 'OnHold') { statusInfo = ' [OnHold]'; } else if (project.status === 'Dropped') { statusInfo = ' [Dropped]'; } // Add due date if present if (project.dueDate) { const dueDateStr = formatCompactDate(project.dueDate); statusInfo += statusInfo ? ` [DUE:${dueDateStr}]` : ` [DUE:${dueDateStr}]`; } // Add flag if present const flaggedSymbol = project.flagged ? ' 🚩' : ''; let projectOutput = `${indent}P: ${project.name}${flaggedSymbol}${statusInfo}\n`; // Process tasks in this project const projectTasks = database.tasks.filter((task: any) => task.projectId === project.id && !task.parentId ); if (projectTasks.length > 0) { for (const task of projectTasks) { projectOutput += processTask(task, level + 1); } } return projectOutput; } // Process a task function processTask(task: any, level: number): string { const indent = ' '.repeat(level); // Skip if it's completed or dropped and we're hiding completed items if (hideCompleted && (task.completed || task.taskStatus === 'Completed' || task.taskStatus === 'Dropped')) { return ''; } // Flag symbol const flagSymbol = task.flagged ? '🚩 ' : ''; // Format dates let dateInfo = ''; if (task.dueDate) { const dueDateStr = formatCompactDate(task.dueDate); dateInfo += ` [DUE:${dueDateStr}]`; } if (task.deferDate) { const deferDateStr = formatCompactDate(task.deferDate); dateInfo += ` [defer:${deferDateStr}]`; } // Format duration let durationStr = ''; if (task.estimatedMinutes) { // Convert to hours if >= 60 minutes if (task.estimatedMinutes >= 60) { const hours = Math.floor(task.estimatedMinutes / 60); durationStr = ` (${hours}h)`; } else { durationStr = ` (${task.estimatedMinutes}m)`; } } // Format tags let tagsStr = ''; if (task.tagNames && task.tagNames.length > 0) { // Use minimum unique prefixes for tag names const abbreviatedTags = task.tagNames.map((tag: string) => { return tagPrefixMap.get(tag) || tag; }); tagsStr = ` <${abbreviatedTags.join(',')}>`; } // Format status let statusStr = ''; switch (task.taskStatus) { case 'Next': statusStr = ' #next'; break; case 'Available': statusStr = ' #avail'; break; case 'Blocked': statusStr = ' #block'; break; case 'DueSoon': statusStr = ' #due'; break; case 'Overdue': statusStr = ' #over'; break; case 'Completed': statusStr = ' #compl'; break; case 'Dropped': statusStr = ' #drop'; break; } let taskOutput = `${indent}• ${flagSymbol}${task.name}${dateInfo}${durationStr}${tagsStr}${statusStr}\n`; // Process subtasks if (task.childIds && task.childIds.length > 0) { const childTasks = database.tasks.filter((t: any) => task.childIds.includes(t.id)); for (const childTask of childTasks) { taskOutput += processTask(childTask, level + 1); } } return taskOutput; } // Process all root folders for (const folder of rootFolders) { output += processFolder(folder, 0); } // Process projects not in any folder (if any) const rootProjects = Object.values(database.projects).filter((project: any) => !project.folderID); for (const project of rootProjects) { output += processProject(project, 0); } // Process tasks in the Inbox (not in any project) const inboxTasks = database.tasks.filter(function (task: any) { return !task.projectId; }); if (inboxTasks.length > 0) { output += `\nP: Inbox\n`; for (const task of inboxTasks) { output += processTask(task, 0); } } return output; } // Compute minimum unique prefixes for all tags (minimum 3 characters) function computeMinimumUniquePrefixes(tagNames: string[]): Map<string, string> { const prefixMap = new Map<string, string>(); // For each tag name for (const tagName of tagNames) { // Start with minimum length of 3 let prefixLength = 3; let isUnique = false; // Keep increasing prefix length until we find a unique prefix while (!isUnique && prefixLength <= tagName.length) { const prefix = tagName.substring(0, prefixLength); // Check if this prefix uniquely identifies the tag isUnique = tagNames.every(otherTag => { // If it's the same tag, skip comparison if (otherTag === tagName) return true; // If the other tag starts with the same prefix, it's not unique return !otherTag.startsWith(prefix); }); if (isUnique) { prefixMap.set(tagName, prefix); } else { prefixLength++; } } // If we couldn't find a unique prefix, use the full tag name if (!isUnique) { prefixMap.set(tagName, tagName); } } return prefixMap; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/themotionmachine/OmniFocus-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server