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>
This commit is contained in:
Eric Jungbauer
2026-04-15 15:32:24 -06:00
commit 7e619d0454
16 changed files with 2783 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env swift
// Fast Reminders query via EventKit handles large reminder databases
// Usage: swift reminders_helper.swift <command> [args...]
// lists list all reminder lists
// get [--list NAME] [--overdue] [--due-today] [--due-this-week] [--limit N]
// search <query> [--limit N]
import EventKit
import Foundation
let store = EKEventStore()
let semaphore = DispatchSemaphore(value: 0)
// Request access
var accessGranted = false
if #available(macOS 14.0, *) {
store.requestFullAccessToReminders { granted, error in
accessGranted = granted
semaphore.signal()
}
} else {
store.requestAccess(to: .reminder) { granted, error in
accessGranted = granted
semaphore.signal()
}
}
semaphore.wait()
guard accessGranted else {
let err: [String: Any] = ["error": "Reminders access denied. Grant permission in System Settings > Privacy & Security > Reminders."]
print(String(data: try! JSONSerialization.data(withJSONObject: err), encoding: .utf8)!)
exit(1)
}
let args = CommandLine.arguments
let command = args.count > 1 ? args[1] : "lists"
func jsonDate(_ date: Date?) -> Any {
guard let d = date else { return NSNull() }
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime]
return f.string(from: d)
}
func printJSON(_ obj: Any) {
let data = try! JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted, .sortedKeys])
print(String(data: data, encoding: .utf8)!)
}
func getCalendars() -> [EKCalendar] {
return store.calendars(for: .reminder)
}
func argValue(_ flag: String) -> String? {
if let idx = args.firstIndex(of: flag), idx + 1 < args.count {
return args[idx + 1]
}
return nil
}
func hasFlag(_ flag: String) -> Bool {
return args.contains(flag)
}
switch command {
case "lists":
let cals = getCalendars()
let result = cals.map { cal -> [String: Any] in
// Count incomplete reminders using a predicate
let pred = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: [cal])
var count = 0
let countSem = DispatchSemaphore(value: 0)
store.fetchReminders(matching: pred) { reminders in
count = reminders?.count ?? 0
countSem.signal()
}
countSem.wait()
return ["name": cal.title, "id": cal.calendarIdentifier, "incomplete_count": count]
}
printJSON(result)
case "get":
let listName = argValue("--list")
let isOverdue = hasFlag("--overdue")
let isDueToday = hasFlag("--due-today")
let isDueThisWeek = hasFlag("--due-this-week")
let limit = Int(argValue("--limit") ?? "50") ?? 50
var calendars: [EKCalendar]? = nil
if let name = listName {
calendars = getCalendars().filter { $0.title.lowercased() == name.lowercased() }
if calendars?.isEmpty == true { calendars = nil }
}
let now = Date()
let calendar = Calendar.current
var startDate: Date? = nil
var endDate: Date? = nil
if isOverdue {
// Reminders due before now
endDate = now
startDate = calendar.date(byAdding: .year, value: -10, to: now)
} else if isDueToday {
startDate = calendar.startOfDay(for: now)
endDate = calendar.date(byAdding: .day, value: 1, to: startDate!)
} else if isDueThisWeek {
startDate = calendar.startOfDay(for: now)
endDate = calendar.date(byAdding: .day, value: 7, to: startDate!)
}
let pred = store.predicateForIncompleteReminders(
withDueDateStarting: startDate,
ending: endDate,
calendars: calendars
)
let fetchSem = DispatchSemaphore(value: 0)
var fetchedReminders: [EKReminder] = []
store.fetchReminders(matching: pred) { reminders in
fetchedReminders = reminders ?? []
fetchSem.signal()
}
fetchSem.wait()
// Sort by due date (overdue first, then soonest)
fetchedReminders.sort { a, b in
let da = a.dueDateComponents?.date ?? Date.distantFuture
let db = b.dueDateComponents?.date ?? Date.distantFuture
return da < db
}
let limited = Array(fetchedReminders.prefix(limit))
let result = limited.map { r -> [String: Any] in
let dueComps = r.dueDateComponents
let dueDate: Date? = dueComps?.date
return [
"name": r.title ?? "Untitled",
"list": r.calendar.title,
"due_date": jsonDate(dueDate),
"priority": r.priority,
"completed": r.isCompleted,
"has_notes": (r.notes != nil && !r.notes!.isEmpty),
"id": r.calendarItemIdentifier
]
}
printJSON(["count": fetchedReminders.count, "showing": limited.count, "reminders": result])
case "search":
let query = args.count > 2 ? args[2].lowercased() : ""
let limit = Int(argValue("--limit") ?? "50") ?? 50
guard !query.isEmpty else {
printJSON(["error": "Search query required"])
exit(1)
}
let pred = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: nil)
let fetchSem = DispatchSemaphore(value: 0)
var fetchedReminders: [EKReminder] = []
store.fetchReminders(matching: pred) { reminders in
fetchedReminders = (reminders ?? []).filter {
($0.title ?? "").lowercased().contains(query)
}
fetchSem.signal()
}
fetchSem.wait()
let limited = Array(fetchedReminders.prefix(limit))
let result = limited.map { r -> [String: Any] in
let dueComps = r.dueDateComponents
let dueDate: Date? = dueComps?.date
return [
"name": r.title ?? "Untitled",
"list": r.calendar.title,
"due_date": jsonDate(dueDate),
"completed": r.isCompleted,
"id": r.calendarItemIdentifier
]
}
printJSON(["count": fetchedReminders.count, "showing": limited.count, "reminders": result])
default:
printJSON(["error": "Unknown command: \(command). Use: lists, get, search"])
exit(1)
}