dump_database
Exports the current state of your OmniFocus database, allowing customization to show completed tasks or filter recurring task duplicates.
Instructions
Gets the current state of your OmniFocus database
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| hideCompleted | No | Set to false to show completed and dropped tasks (default: true) | |
| hideRecurringDuplicates | No | Set to true to hide duplicate instances of recurring tasks (default: true) |
Implementation Reference
- MCP tool handler for 'dump_database' that fetches the OmniFocus database using dumpDatabase() and formats it into a compact text report with options to hide completed tasks and recurring duplicates.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 }; } }
- Zod schema defining optional parameters for the dump_database tool: hideCompleted and hideRecurringDuplicates.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)") });
- src/server.ts:25-30 (registration)Registration of the 'dump_database' tool in the MCP server, specifying name, description, schema, and handler.server.tool( "dump_database", "Gets the current state of your OmniFocus database", dumpDatabaseTool.schema.shape, dumpDatabaseTool.handler );
- src/tools/dumpDatabase.ts:69-195 (helper)Core helper function that executes an OmniFocus AppleScript to dump the full database (tasks, projects, folders, tags) and processes it into a structured OmnifocusDatabase object. Called by the tool handler.export async function dumpDatabase(): Promise<OmnifocusDatabase> { try { // Execute the OmniFocus script const data = await executeOmniFocusScript('@omnifocusDump.js') as OmnifocusDumpData; // wait 1 second await new Promise(resolve => setTimeout(resolve, 1000)); // Create an empty database if no data returned if (!data) { return { exportDate: new Date().toISOString(), tasks: [], projects: {}, folders: {}, tags: {} }; } // Initialize the database object const database: OmnifocusDatabase = { exportDate: data.exportDate, tasks: [], projects: {}, folders: {}, tags: {} }; // Process tasks if (data.tasks && Array.isArray(data.tasks)) { // Convert the tasks to our OmnifocusTask format database.tasks = data.tasks.map((task: OmnifocusDumpTask) => { // Get tag names from the tag IDs const tagNames = (task.tags || []).map(tagId => { return data.tags[tagId]?.name || 'Unknown Tag'; }); return { id: String(task.id), name: String(task.name), note: String(task.note || ""), flagged: Boolean(task.flagged), completed: task.taskStatus === "Completed", completionDate: null, // Not available in the new format dropDate: null, // Not available in the new format taskStatus: String(task.taskStatus), active: task.taskStatus !== "Completed" && task.taskStatus !== "Dropped", dueDate: task.dueDate, deferDate: task.deferDate, estimatedMinutes: task.estimatedMinutes ? Number(task.estimatedMinutes) : null, tags: task.tags || [], tagNames: tagNames, parentId: task.parentTaskID || null, containingProjectId: task.projectID || null, projectId: task.projectID || null, childIds: task.children || [], hasChildren: (task.children && task.children.length > 0) || false, sequential: Boolean(task.sequential), completedByChildren: Boolean(task.completedByChildren), isRepeating: false, // Not available in the new format repetitionMethod: null, // Not available in the new format repetitionRule: null, // Not available in the new format attachments: [], // Default empty array linkedFileURLs: [], // Default empty array notifications: [], // Default empty array shouldUseFloatingTimeZone: false // Default value }; }); } // Process projects if (data.projects) { for (const [id, project] of Object.entries(data.projects)) { database.projects[id] = { id: String(project.id), name: String(project.name), status: String(project.status), folderID: project.folderID || null, sequential: Boolean(project.sequential), effectiveDueDate: project.effectiveDueDate, effectiveDeferDate: project.effectiveDeferDate, dueDate: project.dueDate, deferDate: project.deferDate, completedByChildren: Boolean(project.completedByChildren), containsSingletonActions: Boolean(project.containsSingletonActions), note: String(project.note || ""), tasks: project.tasks || [], flagged: false, // Default value estimatedMinutes: null // Default value }; } } // Process folders if (data.folders) { for (const [id, folder] of Object.entries(data.folders)) { database.folders[id] = { id: String(folder.id), name: String(folder.name), parentFolderID: folder.parentFolderID || null, status: String(folder.status), projects: folder.projects || [], subfolders: folder.subfolders || [] }; } } // Process tags if (data.tags) { for (const [id, tag] of Object.entries(data.tags)) { database.tags[id] = { id: String(tag.id), name: String(tag.name), parentTagID: tag.parentTagID || null, active: Boolean(tag.active), allowsNextAction: Boolean(tag.allowsNextAction), tasks: tag.tasks || [] }; } } return database; } catch (error) { console.error("Error in dumpDatabase:", error); throw error; } }
- Helper function called by the handler to format the raw database into a human-readable compact report with hierarchy, legends, filtering options, and abbreviated tags.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; }