omnifocus-mcp
Version:
Model Context Protocol (MCP) server that integrates with OmniFocus for AI assistant interaction
110 lines (109 loc) • 4.23 kB
JavaScript
import { writeFileSync, unlinkSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { escapeAppleScriptString, escapeForJsonInAppleScript, JSON_ESCAPE_HANDLER, } from '../../utils/appleScriptHelpers.js';
import { runOsascriptFile } from '../../utils/scriptExecution.js';
/**
* Generate pure AppleScript for tag creation
*/
export function generateAppleScript(params) {
const name = escapeAppleScriptString(params.name);
// Resolve the parent tag (by id or name) when nesting is requested.
let parentLookup = '';
let creationTarget = 'make new tag with properties {name:"' + name + '"}';
if (params.parentTagID) {
const parentId = escapeAppleScriptString(params.parentTagID);
// escapeForJsonInAppleScript for the JSON payload — an AppleScript-escaped
// quote would re-materialize raw inside the JSON and corrupt it (#103).
const errorJson = `{\\\"success\\\":false,\\\"error\\\":\\\"Parent tag not found: ${escapeForJsonInAppleScript(params.parentTagID)}\\\"}`;
parentLookup = `
set parentTag to missing value
try
set parentTag to first flattened tag whose id is "${parentId}"
end try
if parentTag is missing value then
return "${errorJson}"
end if`;
creationTarget = 'make new tag with properties {name:"' + name + '"} at end of tags of parentTag';
}
else if (params.parentTagName) {
const parentName = escapeAppleScriptString(params.parentTagName);
const errorJson = `{\\\"success\\\":false,\\\"error\\\":\\\"Parent tag not found: ${escapeForJsonInAppleScript(params.parentTagName)}\\\"}`;
parentLookup = `
set parentTag to missing value
try
set parentTag to first flattened tag where name = "${parentName}"
end try
if parentTag is missing value then
return "${errorJson}"
end if`;
creationTarget = 'make new tag with properties {name:"' + name + '"} at end of tags of parentTag';
}
const script = JSON_ESCAPE_HANDLER + `
try
tell application "OmniFocus"
tell front document
${parentLookup}
set newTag to ${creationTarget}
set tagId to id of newTag as string
-- The name is deliberately NOT echoed: the caller already knows it, and
-- splicing it into hand-built JSON is how quotes corrupted payloads (#103).
return "{\\\"success\\\":true,\\\"tagId\\\":\\"" & tagId & "\\"}"
end tell
end tell
on error errorMessage
return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape(errorMessage) & "\\"}"
end try
`;
return script;
}
/**
* Create a tag in OmniFocus
*/
export async function createTag(params) {
let tempFile;
try {
const script = generateAppleScript(params);
tempFile = join(tmpdir(), `create_tag_${crypto.randomUUID()}.applescript`);
writeFileSync(tempFile, script, { encoding: 'utf8' });
const { stdout, stderr } = await runOsascriptFile(tempFile);
try {
unlinkSync(tempFile);
}
catch { }
if (stderr) {
console.error("AppleScript stderr:", stderr);
}
try {
const result = JSON.parse(stdout);
return {
success: result.success,
tagId: result.tagId,
// The script no longer echoes the name (#103); the caller's input is the
// authoritative value anyway.
name: result.success ? params.name : undefined,
error: result.error
};
}
catch (parseError) {
console.error("Error parsing AppleScript result:", parseError);
return {
success: false,
error: `Failed to parse result: ${stdout}`
};
}
}
catch (error) {
if (tempFile) {
try {
unlinkSync(tempFile);
}
catch { }
}
console.error("Error in createTag:", error);
return {
success: false,
error: error?.message || "Unknown error in createTag"
};
}
}