Skip to main content
Glama
Reminders.swift10.1 kB
import EventKit import Foundation import OSLog import Ontology private let log = Logger.service("reminders") final class RemindersService: Service { private let eventStore = EKEventStore() static let shared = RemindersService() var isActivated: Bool { get async { return EKEventStore.authorizationStatus(for: .reminder) == .fullAccess } } func activate() async throws { try await eventStore.requestFullAccessToReminders() } var tools: [Tool] { Tool( name: "reminders_lists", description: "List available reminder lists", inputSchema: .object( properties: [:], additionalProperties: false ), annotations: .init( title: "List Reminder Lists", readOnlyHint: true, openWorldHint: false ) ) { arguments in guard EKEventStore.authorizationStatus(for: .reminder) == .fullAccess else { log.error("Reminders access not authorized") throw NSError( domain: "RemindersError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Reminders access not authorized"] ) } let reminderLists = self.eventStore.calendars(for: .reminder) return reminderLists.map { reminderList in Value.object([ "title": .string(reminderList.title), "source": .string(reminderList.source.title), "color": .string(reminderList.color.accessibilityName), "isEditable": .bool(reminderList.allowsContentModifications), "isSubscribed": .bool(reminderList.isSubscribed), ]) } } Tool( name: "reminders_fetch", description: "Get reminders from the reminders app with flexible filtering options", inputSchema: .object( properties: [ "completed": .boolean( description: "If true, fetch completed reminders; if false, fetch incomplete; if omitted, fetch all" ), "start": .string( description: "Start date range for fetching reminders", format: .dateTime ), "end": .string( description: "End date range for fetching reminders", format: .dateTime ), "lists": .array( description: "Names of reminder lists to fetch from; if empty, fetches from all lists", items: .string() ), "query": .string( description: "Text to search for in reminder titles" ), ], additionalProperties: false ), annotations: .init( title: "Fetch Reminders", readOnlyHint: true, openWorldHint: false ) ) { arguments in try await self.activate() guard EKEventStore.authorizationStatus(for: .reminder) == .fullAccess else { log.error("Reminders access not authorized") throw NSError( domain: "RemindersError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Reminders access not authorized"] ) } // Filter reminder lists based on provided names var reminderLists = self.eventStore.calendars(for: .reminder) if case let .array(listNames) = arguments["lists"], !listNames.isEmpty { let requestedNames = Set( listNames.compactMap { $0.stringValue?.lowercased() }) reminderLists = reminderLists.filter { requestedNames.contains($0.title.lowercased()) } } // Parse dates if provided var startDate: Date? = nil var endDate: Date? = nil if case let .string(start) = arguments["start"] { startDate = ISO8601DateFormatter.parseFlexibleISODate(start) } if case let .string(end) = arguments["end"] { endDate = ISO8601DateFormatter.parseFlexibleISODate(end) } // Create predicate based on completion status let predicate: NSPredicate if case let .bool(completed) = arguments["completed"] { if completed { predicate = self.eventStore.predicateForCompletedReminders( withCompletionDateStarting: startDate, ending: endDate, calendars: reminderLists ) } else { predicate = self.eventStore.predicateForIncompleteReminders( withDueDateStarting: startDate, ending: endDate, calendars: reminderLists ) } } else { // If completion status not specified, use incomplete predicate as default predicate = self.eventStore.predicateForReminders(in: reminderLists) } // Fetch reminders let reminders = try await withCheckedThrowingContinuation { continuation in self.eventStore.fetchReminders(matching: predicate) { fetchedReminders in continuation.resume(returning: fetchedReminders ?? []) } } // Apply additional filters var filteredReminders = reminders // Filter by search text if provided if case let .string(searchText) = arguments["query"], !searchText.isEmpty { filteredReminders = filteredReminders.filter { $0.title?.localizedCaseInsensitiveContains(searchText) == true } } return filteredReminders.map { PlanAction($0) } } Tool( name: "reminders_create", description: "Create a new reminder with specified properties", inputSchema: .object( properties: [ "title": .string(), "due": .string( format: .dateTime ), "list": .string( description: "Reminder list name (uses default if not specified)" ), "notes": .string(), "priority": .string( default: .string(EKReminderPriority.none.stringValue), enum: EKReminderPriority.allCases.map { .string($0.stringValue) } ), "alarms": .array( description: "Minutes before due date to set alarms", items: .integer() ), ], required: ["title"], additionalProperties: false ), annotations: .init( title: "Create Reminder", destructiveHint: true, openWorldHint: false ) ) { arguments in try await self.activate() guard EKEventStore.authorizationStatus(for: .reminder) == .fullAccess else { log.error("Reminders access not authorized") throw NSError( domain: "RemindersError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Reminders access not authorized"] ) } let reminder = EKReminder(eventStore: self.eventStore) // Set required properties guard case let .string(title) = arguments["title"] else { throw NSError( domain: "RemindersError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Reminder title is required"] ) } reminder.title = title // Set calendar (list) var calendar = self.eventStore.defaultCalendarForNewReminders() if case let .string(listName) = arguments["list"] { if let matchingCalendar = self.eventStore.calendars(for: .reminder) .first(where: { $0.title.lowercased() == listName.lowercased() }) { calendar = matchingCalendar } } reminder.calendar = calendar // Set optional properties if case let .string(dueDateStr) = arguments["due"], let dueDate = ISO8601DateFormatter.parseFlexibleISODate(dueDateStr) { reminder.dueDateComponents = Calendar.current.dateComponents( [.year, .month, .day, .hour, .minute, .second], from: dueDate) } if case let .string(notes) = arguments["notes"] { reminder.notes = notes } if case let .string(priorityStr) = arguments["priority"] { reminder.priority = Int(EKReminderPriority.from(string: priorityStr).rawValue) } // Set alarms if case let .array(alarmMinutes) = arguments["alarms"] { reminder.alarms = alarmMinutes.compactMap { guard case let .int(minutes) = $0 else { return nil } return EKAlarm(relativeOffset: TimeInterval(-minutes * 60)) } } // Save the reminder try self.eventStore.save(reminder, commit: true) return PlanAction(reminder) } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mattt/iMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server