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:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user