omnifocus-mcp
Version:
Model Context Protocol (MCP) server that integrates with OmniFocus for AI assistant interaction
163 lines (158 loc) • 6.55 kB
JavaScript
import { writeFileSync, unlinkSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { createDateOutsideTellBlock } from '../../utils/dateFormatting.js';
import { escapeAppleScriptString, escapeForJsonInAppleScript, generateFolderLookupScript, JSON_ESCAPE_HANDLER, } from '../../utils/appleScriptHelpers.js';
import { repetitionRuleRecord } from '../../utils/repetitionRule.js';
import { runOsascriptFile } from '../../utils/scriptExecution.js';
/**
* Generate pure AppleScript for project creation
*/
export function generateAppleScript(params) {
// Sanitize and prepare parameters for AppleScript
const name = escapeAppleScriptString(params.name);
const note = params.note ? escapeAppleScriptString(params.note, { preserveNewlines: true }) : '';
const dueDate = params.dueDate || '';
const deferDate = params.deferDate || '';
const flagged = params.flagged === true;
const estimatedMinutes = params.estimatedMinutes?.toString() || '';
const tags = params.tags || [];
const sequential = params.sequential === true;
// Generate date constructions outside tell blocks
let datePreScript = '';
let dueDateVar = '';
let deferDateVar = '';
if (dueDate) {
dueDateVar = `dueDate${Math.random().toString(36).substr(2, 9)}`;
datePreScript += createDateOutsideTellBlock(dueDate, dueDateVar) + '\n\n';
}
if (deferDate) {
deferDateVar = `deferDate${Math.random().toString(36).substr(2, 9)}`;
datePreScript += createDateOutsideTellBlock(deferDate, deferDateVar) + '\n\n';
}
// Build project creation block — either at root or in a folder
let projectCreationBlock;
if (params.folderName) {
// escapeForJsonInAppleScript, not escapeAppleScriptString: this name lands
// inside a JSON payload, where an AppleScript-escaped quote re-materializes
// raw and corrupts the result (#103).
const errorJson = `{\\\"success\\\":false,\\\"error\\\":\\\"Folder not found: ${escapeForJsonInAppleScript(params.folderName)}\\\"}`;
const folderLookup = generateFolderLookupScript(params.folderName, 'theFolder', errorJson);
projectCreationBlock = `
-- Find the folder (supports nested paths like "Work/Engineering")
${folderLookup}
set newProject to make new project with properties {name:"${name}"} at end of projects of theFolder`;
}
else {
projectCreationBlock = `
-- Create project at the root level
set newProject to make new project with properties {name:"${name}"}`;
}
// Construct AppleScript with error handling
let script = JSON_ESCAPE_HANDLER + datePreScript + `
try
tell application "OmniFocus"
tell front document
${projectCreationBlock}
-- Set project properties
${note ? `set note of newProject to "${note}"` : ''}
${dueDate ? `
-- Set due date
set due date of newProject to ` + dueDateVar : ''}
${deferDate ? `
-- Set defer date
set defer date of newProject to ` + deferDateVar : ''}
${flagged ? `set flagged of newProject to true` : ''}
${estimatedMinutes ? `set estimated minutes of newProject to ${estimatedMinutes}` : ''}
${`set sequential of newProject to ${sequential}`}
${params.repeat ? `
-- Set the repetition rule (#116). Whole-record assignment only; see
-- the note in addOmniFocusTask.
set repetition rule of newProject to ${repetitionRuleRecord(params.repeat)}` : ''}
-- Get the project ID
set projectId to id of newProject as string
-- Add tags if provided
${tags.length > 0 ? tags.map(tag => {
const sanitizedTag = escapeAppleScriptString(tag);
return `
try
set theTag to first flattened tag where name = "${sanitizedTag}"
add theTag to tags of newProject
on error
-- Tag might not exist, try to create it
try
set theTag to make new tag with properties {name:"${sanitizedTag}"}
add theTag to tags of newProject
on error
-- Could not create or add tag
end try
end try`;
}).join('\n') : ''}
-- Return success with project ID. The name is deliberately NOT echoed:
-- the caller already knows it, and splicing it into hand-built JSON is
-- how quotes in a name corrupted the payload (#103).
return "{\\\"success\\\":true,\\\"projectId\\\":\\"" & projectId & "\\"}"
end tell
end tell
on error errorMessage
return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape(errorMessage) & "\\"}"
end try
`;
return script;
}
/**
* Add a project to OmniFocus
*/
export async function addProject(params) {
let tempFile;
try {
// Generate AppleScript
const script = generateAppleScript(params);
console.error("Executing AppleScript via temp file...");
// Write to a temporary AppleScript file to avoid shell escaping issues
tempFile = join(tmpdir(), `add_project_${crypto.randomUUID()}.applescript`);
writeFileSync(tempFile, script, { encoding: 'utf8' });
// Execute AppleScript from file
const { stdout, stderr } = await runOsascriptFile(tempFile);
// Clean up temp file
try {
unlinkSync(tempFile);
}
catch { }
if (stderr) {
console.error("AppleScript stderr:", stderr);
}
console.error("AppleScript stdout:", stdout);
// Parse the result
try {
const result = JSON.parse(stdout);
// Return the result
return {
success: result.success,
projectId: result.projectId,
error: result.error
};
}
catch (parseError) {
console.error("Error parsing AppleScript result:", parseError);
return {
success: false,
error: `Failed to parse result: ${stdout}`
};
}
}
catch (error) {
// Clean up temp file if it exists
if (tempFile) {
try {
unlinkSync(tempFile);
}
catch { }
}
console.error("Error in addProject:", error);
return {
success: false,
error: error?.message || "Unknown error in addProject"
};
}
}