Files
Eric Jungbauer 7e619d0454 Initial commit — Apple Apps MCP server with native macOS config app
Python MCP server (FastMCP) providing Claude Code/CoWork access to Apple
Reminders, Calendar, Mail, Contacts, Find My, and Maps via AppleScript.
Includes a native SwiftUI config/installer app and a compiled EventKit
helper for fast reminder queries on large databases (8,000+ items).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:32:24 -06:00

288 lines
9.9 KiB
Python

"""Apple Calendar tools."""
from datetime import datetime, timedelta
from helpers import run_applescript, safe_applescript_string, parse_applescript_date, today_str
def list_calendars() -> list[dict]:
"""List all calendars."""
script = '''
tell application "Calendar"
set output to ""
repeat with c in calendars
set cName to name of c
set cDesc to ""
try
set cDesc to description of c
end try
set output to output & cName & "|||" & cDesc & linefeed
end repeat
return output
end tell'''
raw = run_applescript(script)
results = []
for line in raw.strip().split("\n"):
if not line.strip():
continue
parts = line.split("|||")
results.append({
"name": parts[0].strip(),
"description": parts[1].strip() if len(parts) > 1 else "",
})
return results
def get_events(start_date: str | None = None, end_date: str | None = None,
calendar_name: str | None = None, days: int = 7) -> list[dict]:
"""Get calendar events in a date range.
Args:
start_date: ISO date (YYYY-MM-DD). Defaults to today.
end_date: ISO date (YYYY-MM-DD). Defaults to start_date + days.
calendar_name: Filter to a specific calendar.
days: Number of days to look ahead (used if end_date not provided).
"""
if not start_date:
start_date = today_str()
if not end_date:
start_dt = datetime.fromisoformat(start_date)
end_dt = start_dt + timedelta(days=days)
end_date = end_dt.strftime("%Y-%m-%d")
cal_filter = ""
if calendar_name:
safe_cal = safe_applescript_string(calendar_name)
cal_filter = f'set cals to {{calendar "{safe_cal}"}}'
else:
cal_filter = "set cals to calendars"
script = f'''
tell application "Calendar"
{cal_filter}
set startD to current date
set year of startD to {start_date[:4]}
set month of startD to {int(start_date[5:7])}
set day of startD to {int(start_date[8:10])}
set hours of startD to 0
set minutes of startD to 0
set seconds of startD to 0
set endD to current date
set year of endD to {end_date[:4]}
set month of endD to {int(end_date[5:7])}
set day of endD to {int(end_date[8:10])}
set hours of endD to 23
set minutes of endD to 59
set seconds of endD to 59
set output to ""
repeat with c in cals
set calName to name of c
set evts to (every event of c whose start date >= startD and start date <= endD)
repeat with e in evts
set eSummary to summary of e
set eStart to (start date of e) as string
set eEnd to (end date of e) as string
set eLoc to ""
try
set eLoc to location of e
end try
set eAllDay to allday event of e as string
set eNotes to ""
try
set eNotes to description of e
on error
set eNotes to ""
end try
set eId to uid of e
set output to output & calName & ">>>" & eSummary & "|||" & eStart & "|||" & eEnd & "|||" & eLoc & "|||" & eAllDay & "|||" & eNotes & "|||" & eId & linefeed
end repeat
end repeat
return output
end tell'''
raw = run_applescript(script, timeout=60)
results = []
for line in raw.strip().split("\n"):
if not line.strip():
continue
parts = line.split("|||")
if len(parts) < 7:
continue
name_part = parts[0]
cal = None
if ">>>" in name_part:
cal, name_part = name_part.split(">>>", 1)
results.append({
"summary": name_part.strip(),
"calendar": cal.strip() if cal else None,
"start": parse_applescript_date(parts[1].strip()),
"end": parse_applescript_date(parts[2].strip()),
"location": parts[3].strip() or None,
"all_day": parts[4].strip() == "true",
"notes": parts[5].strip() or None,
"id": parts[6].strip(),
})
return results
def search_events(query: str, days_back: int = 30, days_forward: int = 30) -> list[dict]:
"""Search for events by summary text."""
start_dt = datetime.now() - timedelta(days=days_back)
end_dt = datetime.now() + timedelta(days=days_forward)
start_date = start_dt.strftime("%Y-%m-%d")
end_date = end_dt.strftime("%Y-%m-%d")
safe_query = safe_applescript_string(query.lower())
script = f'''
tell application "Calendar"
set startD to current date
set year of startD to {start_date[:4]}
set month of startD to {int(start_date[5:7])}
set day of startD to {int(start_date[8:10])}
set hours of startD to 0
set minutes of startD to 0
set seconds of startD to 0
set endD to current date
set year of endD to {end_date[:4]}
set month of endD to {int(end_date[5:7])}
set day of endD to {int(end_date[8:10])}
set hours of endD to 23
set minutes of endD to 59
set seconds of endD to 59
set output to ""
repeat with c in calendars
set calName to name of c
set evts to (every event of c whose start date >= startD and start date <= endD)
repeat with e in evts
set eSummary to summary of e
if eSummary contains "{safe_query}" then
set eStart to (start date of e) as string
set eEnd to (end date of e) as string
set eId to uid of e
set output to output & calName & ">>>" & eSummary & "|||" & eStart & "|||" & eEnd & "|||" & eId & linefeed
end if
end repeat
end repeat
return output
end tell'''
raw = run_applescript(script, timeout=60)
results = []
for line in raw.strip().split("\n"):
if not line.strip():
continue
parts = line.split("|||")
if len(parts) < 4:
continue
name_part = parts[0]
cal = None
if ">>>" in name_part:
cal, name_part = name_part.split(">>>", 1)
results.append({
"summary": name_part.strip(),
"calendar": cal.strip() if cal else None,
"start": parse_applescript_date(parts[1].strip()),
"end": parse_applescript_date(parts[2].strip()),
"id": parts[3].strip(),
})
return results
def create_event(summary: str, start_date: str, end_date: str | None = None,
calendar_name: str | None = None, location: str | None = None,
notes: str | None = None, all_day: bool = False) -> dict:
"""Create a calendar event.
Args:
summary: Event title.
start_date: ISO datetime (e.g., 2026-04-15T10:00:00).
end_date: ISO datetime. Defaults to 1 hour after start.
calendar_name: Which calendar. Defaults to first calendar.
location: Event location.
notes: Event notes/description.
all_day: Whether this is an all-day event.
"""
if not end_date:
start_dt = datetime.fromisoformat(start_date)
end_dt = start_dt + timedelta(hours=1)
end_date = end_dt.isoformat()
start_dt = datetime.fromisoformat(start_date)
end_dt = datetime.fromisoformat(end_date)
safe_summary = safe_applescript_string(summary)
cal_line = f'set theCal to calendar "{safe_applescript_string(calendar_name)}"' if calendar_name else "set theCal to first calendar"
loc_line = ""
if location:
loc_line = f'set location of newEvent to "{safe_applescript_string(location)}"'
notes_line = ""
if notes:
notes_line = f'set description of newEvent to "{safe_applescript_string(notes)}"'
script = f'''
tell application "Calendar"
{cal_line}
set startD to current date
set year of startD to {start_dt.year}
set month of startD to {start_dt.month}
set day of startD to {start_dt.day}
set hours of startD to {start_dt.hour}
set minutes of startD to {start_dt.minute}
set seconds of startD to 0
set endD to current date
set year of endD to {end_dt.year}
set month of endD to {end_dt.month}
set day of endD to {end_dt.day}
set hours of endD to {end_dt.hour}
set minutes of endD to {end_dt.minute}
set seconds of endD to 0
set newEvent to make new event at end of events of theCal with properties {{summary:"{safe_summary}", start date:startD, end date:endD, allday event:{str(all_day).lower()}}}
{loc_line}
{notes_line}
return uid of newEvent
end tell'''
event_id = run_applescript(script)
return {"id": event_id.strip(), "summary": summary, "start": start_date, "end": end_date, "created": True}
def delete_event(event_summary: str, event_date: str | None = None, calendar_name: str | None = None) -> dict:
"""Delete a calendar event by summary (and optionally date)."""
safe_summary = safe_applescript_string(event_summary)
cal_source = f'calendar "{safe_applescript_string(calendar_name)}"' if calendar_name else "first calendar"
if event_date:
dt = datetime.fromisoformat(event_date)
date_filter = f'''
set targetD to current date
set year of targetD to {dt.year}
set month of targetD to {dt.month}
set day of targetD to {dt.day}
set hours of targetD to 0
set minutes of targetD to 0
set seconds of targetD to 0
set nextD to targetD + (1 * days)
set evts to (every event of theCal whose summary is "{safe_summary}" and start date >= targetD and start date < nextD)'''
else:
date_filter = f'set evts to (every event of theCal whose summary is "{safe_summary}")'
script = f'''
tell application "Calendar"
set theCal to {cal_source}
{date_filter}
if (count of evts) > 0 then
delete item 1 of evts
return "deleted"
else
return "not_found"
end if
end tell'''
result = run_applescript(script)
if result == "deleted":
return {"summary": event_summary, "deleted": True}
raise RuntimeError(f"Event '{event_summary}' not found")