Skip to main content

eventkit/
mcp.rs

1//! EventKit MCP Server
2//!
3//! A Model Context Protocol (MCP) server that exposes macOS Calendar and Reminders
4//! functionality via the EventKit framework.
5//!
6//! This module is gated behind the `mcp` feature flag.
7
8use rmcp::{
9    ErrorData as McpError, RoleServer, ServiceExt, handler::server::wrapper::Parameters, model::*,
10    prompt, prompt_handler, prompt_router, schemars, schemars::JsonSchema, service::RequestContext,
11    tool, tool_handler, tool_router, transport::stdio,
12};
13use serde::{Deserialize, Serialize};
14
15use crate::{EventsManager, RemindersManager};
16use chrono::{DateTime, Duration, Local, NaiveDateTime, TimeZone};
17
18use rmcp::handler::server::wrapper::Json;
19
20// ============================================================================
21// Structured Output Types
22// ============================================================================
23
24/// Convert an EventKitError into an McpError for tool returns.
25fn mcp_err(e: &crate::EventKitError) -> McpError {
26    McpError::internal_error(e.to_string(), None)
27}
28
29fn mcp_invalid(msg: impl std::fmt::Display) -> McpError {
30    McpError::invalid_params(msg.to_string(), None)
31}
32
33#[derive(Serialize, JsonSchema)]
34struct ListResponse<T: Serialize> {
35    #[schemars(with = "i64")]
36    count: usize,
37    items: Vec<T>,
38}
39
40#[derive(Serialize, JsonSchema)]
41struct DeletedResponse {
42    id: String,
43}
44
45#[derive(Serialize, JsonSchema)]
46struct BatchResponse {
47    #[schemars(with = "i64")]
48    total: usize,
49    #[schemars(with = "i64")]
50    succeeded: usize,
51    #[schemars(with = "i64")]
52    failed: usize,
53    #[serde(skip_serializing_if = "Vec::is_empty")]
54    errors: Vec<BatchItemError>,
55}
56
57#[derive(Serialize, JsonSchema)]
58struct BatchItemError {
59    item_id: String,
60    message: String,
61}
62
63#[derive(Serialize, JsonSchema)]
64struct SearchResponse {
65    query: String,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    reminders: Option<ListResponse<ReminderOutput>>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    events: Option<ListResponse<EventOutput>>,
70}
71
72#[derive(Serialize, JsonSchema)]
73struct CoordinateOutput {
74    latitude: f64,
75    longitude: f64,
76}
77
78#[derive(Serialize, JsonSchema)]
79struct LocationOutput {
80    title: String,
81    latitude: f64,
82    longitude: f64,
83    radius_meters: f64,
84}
85
86#[derive(Serialize, JsonSchema)]
87struct AlarmOutput {
88    #[serde(skip_serializing_if = "Option::is_none")]
89    relative_offset_seconds: Option<f64>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    absolute_date: Option<String>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    proximity: Option<String>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    location: Option<LocationOutput>,
96}
97
98impl AlarmOutput {
99    fn from_info(a: &crate::AlarmInfo) -> Self {
100        Self {
101            relative_offset_seconds: a.relative_offset,
102            absolute_date: a.absolute_date.map(|d| d.to_rfc3339()),
103            proximity: match a.proximity {
104                crate::AlarmProximity::Enter => Some("enter".into()),
105                crate::AlarmProximity::Leave => Some("leave".into()),
106                crate::AlarmProximity::None => None,
107            },
108            location: a.location.as_ref().map(|l| LocationOutput {
109                title: l.title.clone(),
110                latitude: l.latitude,
111                longitude: l.longitude,
112                radius_meters: l.radius,
113            }),
114        }
115    }
116}
117
118#[derive(Serialize, JsonSchema)]
119struct RecurrenceRuleOutput {
120    frequency: String,
121    #[schemars(with = "i64")]
122    interval: usize,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    #[schemars(with = "Option<Vec<i32>>")]
125    days_of_week: Option<Vec<u8>>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    days_of_month: Option<Vec<i32>>,
128    end: RecurrenceEndOutput,
129}
130
131#[derive(Serialize, JsonSchema)]
132#[serde(tag = "type", rename_all = "snake_case")]
133enum RecurrenceEndOutput {
134    Never,
135    AfterCount {
136        #[schemars(with = "i64")]
137        count: usize,
138    },
139    OnDate {
140        date: String,
141    },
142}
143
144impl RecurrenceRuleOutput {
145    fn from_rule(r: &crate::RecurrenceRule) -> Self {
146        Self {
147            frequency: match r.frequency {
148                crate::RecurrenceFrequency::Daily => "daily",
149                crate::RecurrenceFrequency::Weekly => "weekly",
150                crate::RecurrenceFrequency::Monthly => "monthly",
151                crate::RecurrenceFrequency::Yearly => "yearly",
152            }
153            .into(),
154            interval: r.interval,
155            days_of_week: r.days_of_week.clone(),
156            days_of_month: r.days_of_month.clone(),
157            end: match &r.end {
158                crate::RecurrenceEndCondition::Never => RecurrenceEndOutput::Never,
159                crate::RecurrenceEndCondition::AfterCount(n) => {
160                    RecurrenceEndOutput::AfterCount { count: *n }
161                }
162                crate::RecurrenceEndCondition::OnDate(d) => RecurrenceEndOutput::OnDate {
163                    date: d.to_rfc3339(),
164                },
165            },
166        }
167    }
168}
169
170#[derive(Serialize, JsonSchema)]
171struct AttendeeOutput {
172    #[serde(skip_serializing_if = "Option::is_none")]
173    name: Option<String>,
174    role: String,
175    status: String,
176    is_current_user: bool,
177}
178
179impl AttendeeOutput {
180    fn from_info(p: &crate::ParticipantInfo) -> Self {
181        Self {
182            name: p.name.clone(),
183            role: format!("{:?}", p.role).to_lowercase(),
184            status: format!("{:?}", p.status).to_lowercase(),
185            is_current_user: p.is_current_user,
186        }
187    }
188}
189
190#[derive(Serialize, JsonSchema)]
191struct ReminderOutput {
192    id: String,
193    title: String,
194    completed: bool,
195    priority: String,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    list_name: Option<String>,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    list_id: Option<String>,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    due_date: Option<String>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    start_date: Option<String>,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    completion_date: Option<String>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    notes: Option<String>,
208    #[serde(skip_serializing_if = "Option::is_none")]
209    url: Option<String>,
210    #[serde(skip_serializing_if = "Vec::is_empty")]
211    tags: Vec<String>,
212    #[serde(skip_serializing_if = "Vec::is_empty")]
213    alarms: Vec<AlarmOutput>,
214    #[serde(skip_serializing_if = "Vec::is_empty")]
215    recurrence_rules: Vec<RecurrenceRuleOutput>,
216}
217
218impl ReminderOutput {
219    fn from_item(r: &crate::ReminderItem, manager: &RemindersManager) -> Self {
220        let alarms = if r.has_alarms {
221            manager
222                .get_alarms(&r.identifier)
223                .unwrap_or_default()
224                .iter()
225                .map(AlarmOutput::from_info)
226                .collect()
227        } else {
228            vec![]
229        };
230        let recurrence_rules = if r.has_recurrence_rules {
231            manager
232                .get_recurrence_rules(&r.identifier)
233                .unwrap_or_default()
234                .iter()
235                .map(RecurrenceRuleOutput::from_rule)
236                .collect()
237        } else {
238            vec![]
239        };
240        Self {
241            tags: r.notes.as_deref().map(extract_tags).unwrap_or_default(),
242            alarms,
243            recurrence_rules,
244            ..Self::from_item_summary(r)
245        }
246    }
247
248    fn from_item_summary(r: &crate::ReminderItem) -> Self {
249        Self {
250            id: r.identifier.clone(),
251            title: r.title.clone(),
252            completed: r.completed,
253            priority: Priority::label(r.priority).into(),
254            list_name: r.calendar_title.clone(),
255            list_id: r.calendar_id.clone(),
256            due_date: r.due_date.map(|d| d.to_rfc3339()),
257            start_date: r.start_date.map(|d| d.to_rfc3339()),
258            completion_date: r.completion_date.map(|d| d.to_rfc3339()),
259            notes: r.notes.clone(),
260            url: r.url.clone(),
261            tags: r.notes.as_deref().map(extract_tags).unwrap_or_default(),
262            alarms: vec![],
263            recurrence_rules: vec![],
264        }
265    }
266}
267
268#[derive(Serialize, JsonSchema)]
269struct EventOutput {
270    id: String,
271    title: String,
272    start: String,
273    end: String,
274    all_day: bool,
275    #[serde(skip_serializing_if = "Option::is_none")]
276    calendar_name: Option<String>,
277    #[serde(skip_serializing_if = "Option::is_none")]
278    calendar_id: Option<String>,
279    #[serde(skip_serializing_if = "Option::is_none")]
280    notes: Option<String>,
281    #[serde(skip_serializing_if = "Option::is_none")]
282    location: Option<String>,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    url: Option<String>,
285    availability: String,
286    status: String,
287    #[serde(skip_serializing_if = "Option::is_none")]
288    structured_location: Option<LocationOutput>,
289    #[serde(skip_serializing_if = "Vec::is_empty")]
290    alarms: Vec<AlarmOutput>,
291    #[serde(skip_serializing_if = "Vec::is_empty")]
292    recurrence_rules: Vec<RecurrenceRuleOutput>,
293    #[serde(skip_serializing_if = "Vec::is_empty")]
294    attendees: Vec<AttendeeOutput>,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    organizer: Option<AttendeeOutput>,
297    is_detached: bool,
298    #[serde(skip_serializing_if = "Option::is_none")]
299    occurrence_date: Option<String>,
300}
301
302impl EventOutput {
303    fn from_item(e: &crate::EventItem, manager: &EventsManager) -> Self {
304        let alarms = manager
305            .get_event_alarms(&e.identifier)
306            .unwrap_or_default()
307            .iter()
308            .map(AlarmOutput::from_info)
309            .collect();
310        let recurrence_rules = manager
311            .get_event_recurrence_rules(&e.identifier)
312            .unwrap_or_default()
313            .iter()
314            .map(RecurrenceRuleOutput::from_rule)
315            .collect();
316        Self {
317            alarms,
318            recurrence_rules,
319            ..Self::from_item_summary(e)
320        }
321    }
322
323    fn from_item_summary(e: &crate::EventItem) -> Self {
324        Self {
325            id: e.identifier.clone(),
326            title: e.title.clone(),
327            start: e.start_date.to_rfc3339(),
328            end: e.end_date.to_rfc3339(),
329            all_day: e.all_day,
330            calendar_name: e.calendar_title.clone(),
331            calendar_id: e.calendar_id.clone(),
332            notes: e.notes.clone(),
333            location: e.location.clone(),
334            url: e.url.clone(),
335            availability: match e.availability {
336                crate::EventAvailability::Busy => "busy",
337                crate::EventAvailability::Free => "free",
338                crate::EventAvailability::Tentative => "tentative",
339                crate::EventAvailability::Unavailable => "unavailable",
340                _ => "not_supported",
341            }
342            .into(),
343            status: match e.status {
344                crate::EventStatus::Confirmed => "confirmed",
345                crate::EventStatus::Tentative => "tentative",
346                crate::EventStatus::Canceled => "canceled",
347                _ => "none",
348            }
349            .into(),
350            structured_location: e.structured_location.as_ref().map(|l| LocationOutput {
351                title: l.title.clone(),
352                latitude: l.latitude,
353                longitude: l.longitude,
354                radius_meters: l.radius,
355            }),
356            alarms: vec![],
357            recurrence_rules: vec![],
358            attendees: e.attendees.iter().map(AttendeeOutput::from_info).collect(),
359            organizer: e.organizer.as_ref().map(AttendeeOutput::from_info),
360            is_detached: e.is_detached,
361            occurrence_date: e.occurrence_date.map(|d| d.to_rfc3339()),
362        }
363    }
364}
365
366#[derive(Serialize, JsonSchema)]
367struct CalendarOutput {
368    id: String,
369    title: String,
370    color: String,
371    #[serde(skip_serializing_if = "Option::is_none")]
372    source: Option<String>,
373    #[serde(skip_serializing_if = "Option::is_none")]
374    source_id: Option<String>,
375    allows_modifications: bool,
376    is_immutable: bool,
377    is_subscribed: bool,
378    entity_types: Vec<String>,
379}
380
381impl CalendarOutput {
382    fn from_info(c: &crate::CalendarInfo) -> Self {
383        Self {
384            id: c.identifier.clone(),
385            title: c.title.clone(),
386            color: c
387                .color
388                .map(|(r, g, b, _)| CalendarColor::from_rgba(r, g, b).to_string())
389                .unwrap_or_else(|| "none".into()),
390            source: c.source.clone(),
391            source_id: c.source_id.clone(),
392            allows_modifications: c.allows_modifications,
393            is_immutable: c.is_immutable,
394            is_subscribed: c.is_subscribed,
395            entity_types: c.allowed_entity_types.clone(),
396        }
397    }
398}
399
400#[derive(Serialize, JsonSchema)]
401struct SourceOutput {
402    id: String,
403    title: String,
404    source_type: String,
405}
406
407impl SourceOutput {
408    fn from_info(s: &crate::SourceInfo) -> Self {
409        Self {
410            id: s.identifier.clone(),
411            title: s.title.clone(),
412            source_type: s.source_type.clone(),
413        }
414    }
415}
416
417// ============================================================================
418// Shared Enums
419// ============================================================================
420
421/// Priority level for reminders.
422#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
423#[serde(rename_all = "lowercase")]
424pub enum Priority {
425    /// No priority (0)
426    None,
427    /// Low priority (9)
428    Low,
429    /// Medium priority (5)
430    Medium,
431    /// High priority (1) — also shows as "flagged" in Reminders.app
432    High,
433}
434
435impl Priority {
436    fn to_usize(&self) -> usize {
437        match self {
438            Priority::None => 0,
439            Priority::Low => 9,
440            Priority::Medium => 5,
441            Priority::High => 1,
442        }
443    }
444
445    fn label(v: usize) -> &'static str {
446        match v {
447            1..=4 => "high",
448            5 => "medium",
449            6..=9 => "low",
450            _ => "none",
451        }
452    }
453}
454
455/// Item type discriminator for unified tools.
456#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
457#[serde(rename_all = "lowercase")]
458pub enum ItemType {
459    Reminder,
460    Event,
461}
462
463// ============================================================================
464// Inline Alarm & Recurrence Param Types
465// ============================================================================
466
467/// Alarm configuration for inline use in create/update.
468#[derive(Debug, Serialize, Deserialize, JsonSchema)]
469pub struct AlarmParam {
470    /// Offset in seconds before the due date (negative = before, e.g., -600 = 10 minutes before)
471    pub relative_offset: Option<f64>,
472    /// Proximity trigger: "enter" or "leave" (for location-based alarms on reminders)
473    pub proximity: Option<String>,
474    /// Title of the location for geofenced alarms
475    pub location_title: Option<String>,
476    /// Latitude of the location
477    pub latitude: Option<f64>,
478    /// Longitude of the location
479    pub longitude: Option<f64>,
480    /// Geofence radius in meters (default: 100)
481    pub radius: Option<f64>,
482}
483
484/// Recurrence configuration for inline use in create/update.
485#[derive(Debug, Serialize, Deserialize, JsonSchema)]
486pub struct RecurrenceParam {
487    /// Frequency: "daily", "weekly", "monthly", or "yearly"
488    pub frequency: String,
489    /// Repeat every N intervals (e.g., 2 = every 2 weeks). Default: 1
490    #[serde(default = "default_interval")]
491    #[schemars(with = "i64")]
492    pub interval: usize,
493    /// Days of the week (1=Sun, 2=Mon, ..., 7=Sat) for weekly/monthly rules
494    #[schemars(with = "Option<Vec<i32>>")]
495    pub days_of_week: Option<Vec<u8>>,
496    /// Days of the month (1-31) for monthly rules
497    pub days_of_month: Option<Vec<i32>>,
498    /// End after this many occurrences (mutually exclusive with end_date)
499    #[schemars(with = "Option<i64>")]
500    pub end_after_count: Option<usize>,
501    /// End on this date in format 'YYYY-MM-DD' (mutually exclusive with end_after_count)
502    pub end_date: Option<String>,
503}
504
505// ============================================================================
506// Request/Response Types for Tools
507// ============================================================================
508
509#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
510pub struct ListRemindersRequest {
511    /// If true, show all reminders including completed ones. Default: false
512    #[serde(default)]
513    pub show_completed: bool,
514    /// Optional: Filter to a specific reminder list by name
515    pub list_name: Option<String>,
516}
517
518#[derive(Debug, Serialize, Deserialize, JsonSchema)]
519pub struct CreateReminderRequest {
520    /// The title/name of the reminder
521    pub title: String,
522    /// The name of the reminder list to add to (REQUIRED - use list_reminder_lists to see available lists)
523    pub list_name: String,
524    /// Optional notes/description for the reminder
525    pub notes: Option<String>,
526    /// Priority: "none", "low", "medium", "high" (high = flagged)
527    pub priority: Option<Priority>,
528    /// Optional due date in format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM'. If only time 'HH:MM' is given, today's date is used.
529    pub due_date: Option<String>,
530    /// Optional start date when to begin working (format: 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM')
531    pub start_date: Option<String>,
532    /// Optional URL to associate with the reminder
533    pub url: Option<String>,
534    /// Optional alarms (replaces all existing). Each alarm can be time-based or location-based.
535    pub alarms: Option<Vec<AlarmParam>>,
536    /// Optional recurrence rule (replaces existing)
537    pub recurrence: Option<RecurrenceParam>,
538    /// Optional tags (stored as #tagname in notes). Replaces existing tags when provided.
539    pub tags: Option<Vec<String>>,
540}
541
542#[derive(Debug, Serialize, Deserialize, JsonSchema)]
543pub struct UpdateReminderRequest {
544    /// The unique identifier of the reminder to update
545    pub reminder_id: String,
546    /// The name of the reminder list to move this reminder to
547    pub list_name: Option<String>,
548    /// New title for the reminder
549    pub title: Option<String>,
550    /// New notes for the reminder
551    pub notes: Option<String>,
552    /// Mark as completed (true) or incomplete (false)
553    pub completed: Option<bool>,
554    /// Priority: "none", "low", "medium", "high" (high = flagged)
555    pub priority: Option<Priority>,
556    /// New due date in format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM'. Set to empty string to clear.
557    pub due_date: Option<String>,
558    /// New start date. Set to empty string to clear.
559    pub start_date: Option<String>,
560    /// URL to associate (set to empty string to clear)
561    pub url: Option<String>,
562    /// Alarms (replaces all existing when provided). Pass empty array to clear.
563    pub alarms: Option<Vec<AlarmParam>>,
564    /// Recurrence rule (replaces existing when provided). Omit to keep, set frequency to "" to clear.
565    pub recurrence: Option<RecurrenceParam>,
566    /// Tags (stored as #tagname in notes). Replaces existing tags when provided.
567    pub tags: Option<Vec<String>>,
568}
569
570#[derive(Debug, Serialize, Deserialize, JsonSchema)]
571pub struct CreateReminderListRequest {
572    /// The name of the new reminder list to create
573    pub name: String,
574}
575
576/// Predefined colors for calendars and reminder lists.
577#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
578#[serde(rename_all = "lowercase")]
579pub enum CalendarColor {
580    Red,
581    Orange,
582    Yellow,
583    Green,
584    Blue,
585    Purple,
586    Brown,
587    Pink,
588    Teal,
589}
590
591impl CalendarColor {
592    fn to_rgba(&self) -> (f64, f64, f64, f64) {
593        match self {
594            CalendarColor::Red => (1.0, 0.231, 0.188, 1.0),
595            CalendarColor::Orange => (1.0, 0.584, 0.0, 1.0),
596            CalendarColor::Yellow => (1.0, 0.8, 0.0, 1.0),
597            CalendarColor::Green => (0.298, 0.851, 0.392, 1.0),
598            CalendarColor::Blue => (0.0, 0.478, 1.0, 1.0),
599            CalendarColor::Purple => (0.686, 0.322, 0.871, 1.0),
600            CalendarColor::Brown => (0.635, 0.518, 0.369, 1.0),
601            CalendarColor::Pink => (1.0, 0.176, 0.333, 1.0),
602            CalendarColor::Teal => (0.353, 0.784, 0.98, 1.0),
603        }
604    }
605
606    /// Find the closest named color for an RGBA value.
607    fn from_rgba(r: f64, g: f64, b: f64) -> &'static str {
608        let colors: &[(&str, (f64, f64, f64))] = &[
609            ("red", (1.0, 0.231, 0.188)),
610            ("orange", (1.0, 0.584, 0.0)),
611            ("yellow", (1.0, 0.8, 0.0)),
612            ("green", (0.298, 0.851, 0.392)),
613            ("blue", (0.0, 0.478, 1.0)),
614            ("purple", (0.686, 0.322, 0.871)),
615            ("brown", (0.635, 0.518, 0.369)),
616            ("pink", (1.0, 0.176, 0.333)),
617            ("teal", (0.353, 0.784, 0.98)),
618        ];
619        colors
620            .iter()
621            .min_by(|(_, a), (_, b_)| {
622                let da = (a.0 - r).powi(2) + (a.1 - g).powi(2) + (a.2 - b).powi(2);
623                let db = (b_.0 - r).powi(2) + (b_.1 - g).powi(2) + (b_.2 - b).powi(2);
624                da.partial_cmp(&db).unwrap()
625            })
626            .map(|(name, _)| *name)
627            .unwrap_or("blue")
628    }
629}
630
631#[derive(Debug, Serialize, Deserialize, JsonSchema)]
632pub struct UpdateReminderListRequest {
633    /// The unique identifier of the reminder list to update
634    pub list_id: String,
635    /// New name for the list (optional)
636    pub name: Option<String>,
637    /// Color for the list (optional). Use a color name or custom hex.
638    pub color: Option<CalendarColor>,
639}
640
641#[derive(Debug, Serialize, Deserialize, JsonSchema)]
642pub struct UpdateEventCalendarRequest {
643    /// The unique identifier of the event calendar to update
644    pub calendar_id: String,
645    /// New name for the calendar (optional)
646    pub name: Option<String>,
647    /// Color for the calendar (optional). Use a color name or custom hex.
648    pub color: Option<CalendarColor>,
649}
650
651#[derive(Debug, Serialize, Deserialize, JsonSchema)]
652pub struct DeleteReminderListRequest {
653    /// The unique identifier of the reminder list to delete
654    pub list_id: String,
655}
656
657#[derive(Debug, Serialize, Deserialize, JsonSchema)]
658pub struct ReminderIdRequest {
659    /// The unique identifier of the reminder
660    pub reminder_id: String,
661}
662
663#[derive(Debug, Serialize, Deserialize, JsonSchema)]
664pub struct ListEventsRequest {
665    /// Number of days from today to include (default: 1 for today only)
666    #[serde(default = "default_days")]
667    pub days: i64,
668    /// Optional: Filter to a specific calendar by ID (use list_calendars to get IDs)
669    pub calendar_id: Option<String>,
670}
671
672fn default_days() -> i64 {
673    1
674}
675
676#[derive(Debug, Serialize, Deserialize, JsonSchema)]
677pub struct CreateEventRequest {
678    /// The title of the event
679    pub title: String,
680    /// Start date/time in format 'YYYY-MM-DD HH:MM' or 'YYYY-MM-DD' for all-day events
681    pub start: String,
682    /// End date/time in format 'YYYY-MM-DD HH:MM'. If not specified, uses duration_minutes.
683    pub end: Option<String>,
684    /// Duration in minutes (default: 60). Used if end is not specified.
685    #[serde(default = "default_duration")]
686    pub duration_minutes: i64,
687    /// Optional notes/description for the event
688    pub notes: Option<String>,
689    /// Optional location for the event
690    pub location: Option<String>,
691    /// Optional: The name of the calendar to add to
692    pub calendar_name: Option<String>,
693    /// Whether this is an all-day event
694    #[serde(default)]
695    pub all_day: bool,
696    /// Optional URL to associate with the event
697    pub url: Option<String>,
698    /// Optional alarms (replaces all existing). Time-based only for events.
699    pub alarms: Option<Vec<AlarmParam>>,
700    /// Optional recurrence rule
701    pub recurrence: Option<RecurrenceParam>,
702}
703
704fn default_duration() -> i64 {
705    60
706}
707
708#[derive(Debug, Serialize, Deserialize, JsonSchema)]
709pub struct EventIdRequest {
710    /// The unique identifier of the event
711    pub event_id: String,
712    /// If true, affect this event and all future occurrences (for recurring events)
713    #[serde(default)]
714    pub affect_future: bool,
715}
716
717#[derive(Debug, Serialize, Deserialize, JsonSchema)]
718pub struct UpdateEventRequest {
719    /// The unique identifier of the event to update
720    pub event_id: String,
721    /// New title for the event
722    pub title: Option<String>,
723    /// New notes for the event
724    pub notes: Option<String>,
725    /// New location for the event
726    pub location: Option<String>,
727    /// New start date/time in format 'YYYY-MM-DD HH:MM'
728    pub start: Option<String>,
729    /// New end date/time in format 'YYYY-MM-DD HH:MM'
730    pub end: Option<String>,
731    /// URL to associate (set to empty string to clear)
732    pub url: Option<String>,
733    /// Alarms (replaces all existing when provided). Pass empty array to clear.
734    pub alarms: Option<Vec<AlarmParam>>,
735    /// Recurrence rule (replaces existing when provided)
736    pub recurrence: Option<RecurrenceParam>,
737}
738
739// ============================================================================
740// Batch Operation Request Types
741// ============================================================================
742
743#[derive(Debug, Serialize, Deserialize, JsonSchema)]
744pub struct BatchDeleteRequest {
745    /// Whether these are "reminder" or "event" items
746    pub item_type: ItemType,
747    /// List of item IDs to delete
748    pub item_ids: Vec<String>,
749    /// For recurring events: if true, delete this and all future occurrences
750    #[serde(default)]
751    pub affect_future: bool,
752}
753
754#[derive(Debug, Serialize, Deserialize, JsonSchema)]
755pub struct BatchMoveRequest {
756    /// List of reminder IDs to move
757    pub reminder_ids: Vec<String>,
758    /// The name of the destination reminder list
759    pub destination_list_name: String,
760}
761
762#[derive(Debug, Serialize, Deserialize, JsonSchema)]
763pub struct BatchUpdateItem {
764    /// The unique identifier of the item to update
765    pub item_id: String,
766    /// New title
767    pub title: Option<String>,
768    /// New notes
769    pub notes: Option<String>,
770    /// Mark completed (reminders only)
771    pub completed: Option<bool>,
772    /// Priority (reminders only)
773    pub priority: Option<Priority>,
774    /// Due date (reminders only)
775    pub due_date: Option<String>,
776}
777
778#[derive(Debug, Serialize, Deserialize, JsonSchema)]
779pub struct BatchUpdateRequest {
780    /// Whether these are "reminder" or "event" items
781    pub item_type: ItemType,
782    /// List of updates to apply
783    pub updates: Vec<BatchUpdateItem>,
784}
785
786#[derive(Debug, Serialize, Deserialize, JsonSchema)]
787pub struct SearchRequest {
788    /// Text to search for in titles and notes (case-insensitive)
789    pub query: String,
790    /// Whether to search "reminder" or "event" items. If omitted, searches both.
791    pub item_type: Option<ItemType>,
792    /// For reminders: if true, also search completed reminders. Default: false
793    #[serde(default)]
794    pub include_completed: bool,
795    /// For events: number of days from today to search (default: 30)
796    #[serde(default = "default_search_days")]
797    pub days: i64,
798}
799
800fn default_search_days() -> i64 {
801    30
802}
803
804fn default_interval() -> usize {
805    1
806}
807
808// ============================================================================
809// Prompt Argument Types
810// ============================================================================
811
812#[derive(Debug, Serialize, Deserialize, JsonSchema)]
813pub struct ListRemindersPromptArgs {
814    /// Name of the reminder list to show. If not provided, shows all lists.
815    #[serde(default)]
816    pub list_name: Option<String>,
817}
818
819#[derive(Debug, Serialize, Deserialize, JsonSchema)]
820pub struct MoveReminderPromptArgs {
821    /// The unique identifier of the reminder to move
822    pub reminder_id: String,
823    /// The name of the destination reminder list
824    pub destination_list: String,
825}
826
827#[derive(Debug, Serialize, Deserialize, JsonSchema)]
828pub struct CreateReminderPromptArgs {
829    /// Title of the reminder
830    pub title: String,
831    /// Detailed notes/context for the reminder
832    #[serde(default)]
833    pub notes: Option<String>,
834    /// Name of the reminder list to add to
835    #[serde(default)]
836    pub list_name: Option<String>,
837    /// Priority (0 = none, 1-4 = high, 5 = medium, 6-9 = low)
838    #[serde(default)]
839    #[schemars(with = "Option<i32>")]
840    pub priority: Option<u8>,
841    /// Due date in format "YYYY-MM-DD" or "YYYY-MM-DD HH:MM"
842    #[serde(default)]
843    pub due_date: Option<String>,
844}
845
846// ============================================================================
847// EventKit MCP Server
848// ============================================================================
849
850/// EventKit MCP Server - provides access to macOS Calendar and Reminders
851/// Note: EventKit managers are created fresh in each tool call as they are not Send+Sync
852#[derive(Clone)]
853pub struct EventKitServer {
854    /// Limits concurrent EventKit access to 1 at a time, since EventKit is !Send+!Sync
855    concurrency: std::sync::Arc<tokio::sync::Semaphore>,
856}
857
858impl Default for EventKitServer {
859    fn default() -> Self {
860        Self::new()
861    }
862}
863
864/// Parse a date string in format "YYYY-MM-DD" or "YYYY-MM-DD HH:MM"
865fn parse_datetime(s: &str) -> Result<DateTime<Local>, String> {
866    // Try parsing with time first
867    if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M") {
868        return Local
869            .from_local_datetime(&dt)
870            .single()
871            .ok_or_else(|| "Invalid local datetime".to_string());
872    }
873
874    // Try parsing date only
875    if let Ok(date) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
876        let dt = date
877            .and_hms_opt(0, 0, 0)
878            .ok_or_else(|| "Invalid date".to_string())?;
879        return Local
880            .from_local_datetime(&dt)
881            .single()
882            .ok_or_else(|| "Invalid local datetime".to_string());
883    }
884
885    Err("Invalid date format. Use 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM'".to_string())
886}
887
888#[tool_router]
889impl EventKitServer {
890    pub fn new() -> Self {
891        Self {
892            concurrency: std::sync::Arc::new(tokio::sync::Semaphore::new(1)),
893        }
894    }
895
896    // ========================================================================
897    // Reminders Tools
898    // ========================================================================
899
900    #[tool(description = "List all reminder lists (calendars) available in macOS Reminders.")]
901    async fn list_reminder_lists(&self) -> Result<Json<ListResponse<CalendarOutput>>, McpError> {
902        let _permit = self.concurrency.acquire().await.unwrap();
903        let manager = RemindersManager::new();
904        match manager.list_calendars() {
905            Ok(lists) => {
906                let items: Vec<_> = lists.iter().map(CalendarOutput::from_info).collect();
907                Ok(Json(ListResponse {
908                    count: items.len(),
909                    items,
910                }))
911            }
912            Err(e) => Err(mcp_err(&e)),
913        }
914    }
915
916    #[tool(
917        description = "List reminders from macOS Reminders app. Can filter by completion status."
918    )]
919    async fn list_reminders(
920        &self,
921        Parameters(params): Parameters<ListRemindersRequest>,
922    ) -> Result<Json<ListResponse<ReminderOutput>>, McpError> {
923        let _permit = self.concurrency.acquire().await.unwrap();
924        let manager = RemindersManager::new();
925
926        let reminders = if params.show_completed {
927            manager.fetch_all_reminders()
928        } else {
929            manager.fetch_incomplete_reminders()
930        };
931
932        match reminders {
933            Ok(items) => {
934                let filtered: Vec<_> = if let Some(name) = params.list_name {
935                    items
936                        .into_iter()
937                        .filter(|r| r.calendar_title.as_deref() == Some(&name))
938                        .collect()
939                } else {
940                    items
941                };
942                let items: Vec<_> = filtered
943                    .iter()
944                    .map(ReminderOutput::from_item_summary)
945                    .collect();
946                Ok(Json(ListResponse {
947                    count: items.len(),
948                    items,
949                }))
950            }
951            Err(e) => Err(mcp_err(&e)),
952        }
953    }
954
955    #[tool(
956        description = "Create a new reminder in macOS Reminders. You MUST specify which list to add it to. Use list_reminder_lists first to see available lists. Can include alarms, recurrence, and URL inline."
957    )]
958    async fn create_reminder(
959        &self,
960        Parameters(params): Parameters<CreateReminderRequest>,
961    ) -> Result<Json<ReminderOutput>, McpError> {
962        let _permit = self.concurrency.acquire().await.unwrap();
963        let manager = RemindersManager::new();
964
965        // Validate the list exists
966        let calendar_title = match manager.list_calendars() {
967            Ok(lists) => {
968                if let Some(cal) = lists.iter().find(|c| c.title == params.list_name) {
969                    cal.title.clone()
970                } else {
971                    let available: Vec<_> = lists.iter().map(|c| c.title.as_str()).collect();
972                    return Err(mcp_invalid(format!(
973                        "List '{}' not found. Available lists: {}",
974                        params.list_name,
975                        available.join(", ")
976                    )));
977                }
978            }
979            Err(e) => {
980                return Err(mcp_invalid(format!("Error listing calendars: {e}")));
981            }
982        };
983
984        let due_date = match params
985            .due_date
986            .as_deref()
987            .map(parse_datetime_or_time)
988            .transpose()
989        {
990            Ok(v) => v,
991            Err(e) => return Err(mcp_invalid(format!("Error parsing due_date: {e}"))),
992        };
993        let start_date = match params.start_date.as_deref().map(parse_datetime).transpose() {
994            Ok(v) => v,
995            Err(e) => return Err(mcp_invalid(format!("Error parsing start_date: {e}"))),
996        };
997
998        let priority = params.priority.as_ref().map(Priority::to_usize);
999
1000        // Merge tags into notes if provided
1001        let notes = if let Some(tags) = &params.tags {
1002            Some(apply_tags(params.notes.as_deref(), tags))
1003        } else {
1004            params.notes.clone()
1005        };
1006
1007        match manager.create_reminder(
1008            &params.title,
1009            notes.as_deref(),
1010            Some(&calendar_title),
1011            priority,
1012            due_date,
1013            start_date,
1014        ) {
1015            Ok(reminder) => {
1016                let id = reminder.identifier.clone();
1017                if let Some(url) = &params.url {
1018                    let _ = manager.set_url(&id, Some(url));
1019                }
1020                if let Some(alarms) = &params.alarms {
1021                    apply_alarms_reminder(&manager, &id, alarms);
1022                }
1023                if let Some(recurrence) = &params.recurrence
1024                    && let Ok(rule) = parse_recurrence_param(recurrence)
1025                {
1026                    let _ = manager.set_recurrence_rule(&id, &rule);
1027                }
1028                let updated = manager.get_reminder(&id).unwrap_or(reminder);
1029                Ok(Json(ReminderOutput::from_item(&updated, &manager)))
1030            }
1031            Err(e) => Err(mcp_err(&e)),
1032        }
1033    }
1034
1035    #[tool(
1036        description = "Update an existing reminder. All fields are optional. Can update alarms, recurrence, and URL inline."
1037    )]
1038    async fn update_reminder(
1039        &self,
1040        Parameters(params): Parameters<UpdateReminderRequest>,
1041    ) -> Result<Json<ReminderOutput>, McpError> {
1042        let _permit = self.concurrency.acquire().await.unwrap();
1043        let manager = RemindersManager::new();
1044
1045        // Parse due date: Some("") means clear, Some(date) means set, None means no change
1046        let due_date = match &params.due_date {
1047            Some(due_str) if due_str.is_empty() => Some(None),
1048            Some(due_str) => match parse_datetime_or_time(due_str) {
1049                Ok(dt) => Some(Some(dt)),
1050                Err(e) => return Err(mcp_invalid(format!("Error parsing due_date: {e}"))),
1051            },
1052            None => None,
1053        };
1054
1055        let start_date = match &params.start_date {
1056            Some(start_str) if start_str.is_empty() => Some(None),
1057            Some(start_str) => match parse_datetime(start_str) {
1058                Ok(dt) => Some(Some(dt)),
1059                Err(e) => return Err(mcp_invalid(format!("Error parsing start_date: {e}"))),
1060            },
1061            None => None,
1062        };
1063
1064        if let Some(ref list_name) = params.list_name {
1065            match manager.list_calendars() {
1066                Ok(lists) => {
1067                    if !lists.iter().any(|c| &c.title == list_name) {
1068                        let available: Vec<_> = lists.iter().map(|c| c.title.as_str()).collect();
1069                        return Err(mcp_invalid(format!(
1070                            "List '{}' not found. Available lists: {}",
1071                            list_name,
1072                            available.join(", ")
1073                        )));
1074                    }
1075                }
1076                Err(e) => return Err(mcp_invalid(format!("Error: {e}"))),
1077            }
1078        }
1079
1080        let priority = params.priority.as_ref().map(Priority::to_usize);
1081
1082        // Merge tags into notes if provided
1083        let notes = if let Some(tags) = &params.tags {
1084            // For update, we need the existing notes to merge with
1085            let existing_notes = manager
1086                .get_reminder(&params.reminder_id)
1087                .ok()
1088                .and_then(|r| r.notes);
1089            let base = params.notes.as_deref().or(existing_notes.as_deref());
1090            Some(apply_tags(base, tags))
1091        } else {
1092            params.notes.clone()
1093        };
1094
1095        match manager.update_reminder(
1096            &params.reminder_id,
1097            params.title.as_deref(),
1098            notes.as_deref(),
1099            params.completed,
1100            priority,
1101            due_date,
1102            start_date,
1103            params.list_name.as_deref(),
1104        ) {
1105            Ok(reminder) => {
1106                let id = reminder.identifier.clone();
1107                if let Some(url) = &params.url {
1108                    let url_val = if url.is_empty() {
1109                        None
1110                    } else {
1111                        Some(url.as_str())
1112                    };
1113                    let _ = manager.set_url(&id, url_val);
1114                }
1115                if let Some(alarms) = &params.alarms {
1116                    apply_alarms_reminder(&manager, &id, alarms);
1117                }
1118                if let Some(recurrence) = &params.recurrence {
1119                    if recurrence.frequency.is_empty() {
1120                        let _ = manager.remove_recurrence_rules(&id);
1121                    } else if let Ok(rule) = parse_recurrence_param(recurrence) {
1122                        let _ = manager.set_recurrence_rule(&id, &rule);
1123                    }
1124                }
1125                let updated = manager.get_reminder(&id).unwrap_or(reminder);
1126                Ok(Json(ReminderOutput::from_item(&updated, &manager)))
1127            }
1128            Err(e) => Err(mcp_err(&e)),
1129        }
1130    }
1131
1132    #[tool(description = "Create a new reminder list (calendar for reminders).")]
1133    async fn create_reminder_list(
1134        &self,
1135        Parameters(params): Parameters<CreateReminderListRequest>,
1136    ) -> Result<Json<CalendarOutput>, McpError> {
1137        let _permit = self.concurrency.acquire().await.unwrap();
1138        let manager = RemindersManager::new();
1139        match manager.create_calendar(&params.name) {
1140            Ok(cal) => Ok(Json(CalendarOutput::from_info(&cal))),
1141            Err(e) => Err(mcp_err(&e)),
1142        }
1143    }
1144
1145    #[tool(description = "Update a reminder list — change name and/or color.")]
1146    async fn update_reminder_list(
1147        &self,
1148        Parameters(params): Parameters<UpdateReminderListRequest>,
1149    ) -> Result<Json<CalendarOutput>, McpError> {
1150        let _permit = self.concurrency.acquire().await.unwrap();
1151        let manager = RemindersManager::new();
1152        let color_rgba = params.color.as_ref().map(CalendarColor::to_rgba);
1153        match manager.update_calendar(&params.list_id, params.name.as_deref(), color_rgba) {
1154            Ok(cal) => Ok(Json(CalendarOutput::from_info(&cal))),
1155            Err(e) => Err(mcp_err(&e)),
1156        }
1157    }
1158
1159    #[tool(
1160        description = "Delete a reminder list. WARNING: This will delete all reminders in the list!"
1161    )]
1162    async fn delete_reminder_list(
1163        &self,
1164        Parameters(params): Parameters<DeleteReminderListRequest>,
1165    ) -> Result<Json<DeletedResponse>, McpError> {
1166        let _permit = self.concurrency.acquire().await.unwrap();
1167        let manager = RemindersManager::new();
1168        match manager.delete_calendar(&params.list_id) {
1169            Ok(_) => Ok(Json(DeletedResponse { id: params.list_id })),
1170            Err(e) => Err(mcp_err(&e)),
1171        }
1172    }
1173
1174    #[tool(description = "Mark a reminder as completed.")]
1175    async fn complete_reminder(
1176        &self,
1177        Parameters(params): Parameters<ReminderIdRequest>,
1178    ) -> Result<Json<ReminderOutput>, McpError> {
1179        let _permit = self.concurrency.acquire().await.unwrap();
1180        let manager = RemindersManager::new();
1181        match manager.complete_reminder(&params.reminder_id) {
1182            Ok(_) => {
1183                let r = manager.get_reminder(&params.reminder_id);
1184                match r {
1185                    Ok(r) => Ok(Json(ReminderOutput::from_item(&r, &manager))),
1186                    Err(e) => Err(mcp_err(&e)),
1187                }
1188            }
1189            Err(e) => Err(mcp_err(&e)),
1190        }
1191    }
1192
1193    #[tool(description = "Mark a reminder as not completed (uncomplete it).")]
1194    async fn uncomplete_reminder(
1195        &self,
1196        Parameters(params): Parameters<ReminderIdRequest>,
1197    ) -> Result<Json<ReminderOutput>, McpError> {
1198        let _permit = self.concurrency.acquire().await.unwrap();
1199        let manager = RemindersManager::new();
1200        match manager.uncomplete_reminder(&params.reminder_id) {
1201            Ok(_) => {
1202                let r = manager.get_reminder(&params.reminder_id);
1203                match r {
1204                    Ok(r) => Ok(Json(ReminderOutput::from_item(&r, &manager))),
1205                    Err(e) => Err(mcp_err(&e)),
1206                }
1207            }
1208            Err(e) => Err(mcp_err(&e)),
1209        }
1210    }
1211
1212    #[tool(description = "Get a single reminder by its unique identifier.")]
1213    async fn get_reminder(
1214        &self,
1215        Parameters(params): Parameters<ReminderIdRequest>,
1216    ) -> Result<Json<ReminderOutput>, McpError> {
1217        let _permit = self.concurrency.acquire().await.unwrap();
1218        let manager = RemindersManager::new();
1219        match manager.get_reminder(&params.reminder_id) {
1220            Ok(r) => Ok(Json(ReminderOutput::from_item(&r, &manager))),
1221            Err(e) => Err(mcp_err(&e)),
1222        }
1223    }
1224
1225    #[tool(description = "Delete a reminder from macOS Reminders.")]
1226    async fn delete_reminder(
1227        &self,
1228        Parameters(params): Parameters<ReminderIdRequest>,
1229    ) -> Result<Json<DeletedResponse>, McpError> {
1230        let _permit = self.concurrency.acquire().await.unwrap();
1231        let manager = RemindersManager::new();
1232        match manager.delete_reminder(&params.reminder_id) {
1233            Ok(_) => Ok(Json(DeletedResponse {
1234                id: params.reminder_id,
1235            })),
1236            Err(e) => Err(mcp_err(&e)),
1237        }
1238    }
1239
1240    // ========================================================================
1241    // Calendar/Events Tools
1242    // ========================================================================
1243
1244    #[tool(description = "List all calendars available in macOS Calendar app.")]
1245    async fn list_calendars(&self) -> Result<Json<ListResponse<CalendarOutput>>, McpError> {
1246        let _permit = self.concurrency.acquire().await.unwrap();
1247        let manager = EventsManager::new();
1248        match manager.list_calendars() {
1249            Ok(cals) => {
1250                let items: Vec<_> = cals.iter().map(CalendarOutput::from_info).collect();
1251                Ok(Json(ListResponse {
1252                    count: items.len(),
1253                    items,
1254                }))
1255            }
1256            Err(e) => Err(mcp_err(&e)),
1257        }
1258    }
1259
1260    #[tool(
1261        description = "List calendar events. By default shows today's events. Can specify a date range."
1262    )]
1263    async fn list_events(
1264        &self,
1265        Parameters(params): Parameters<ListEventsRequest>,
1266    ) -> Result<Json<ListResponse<EventOutput>>, McpError> {
1267        let _permit = self.concurrency.acquire().await.unwrap();
1268        let manager = EventsManager::new();
1269
1270        let events = if params.days == 1 {
1271            manager.fetch_today_events()
1272        } else {
1273            let start = Local::now();
1274            let end = start + Duration::days(params.days);
1275            manager.fetch_events(start, end, None)
1276        };
1277
1278        match events {
1279            Ok(items) => {
1280                let filtered: Vec<_> = if let Some(ref cal_id) = params.calendar_id {
1281                    items
1282                        .into_iter()
1283                        .filter(|e| e.calendar_id.as_deref() == Some(cal_id.as_str()))
1284                        .collect()
1285                } else {
1286                    items
1287                };
1288                let items: Vec<_> = filtered
1289                    .iter()
1290                    .map(EventOutput::from_item_summary)
1291                    .collect();
1292                Ok(Json(ListResponse {
1293                    count: items.len(),
1294                    items,
1295                }))
1296            }
1297            Err(e) => Err(mcp_err(&e)),
1298        }
1299    }
1300
1301    #[tool(
1302        description = "Create a new calendar event in macOS Calendar. Can include alarms, recurrence, and URL inline."
1303    )]
1304    async fn create_event(
1305        &self,
1306        Parameters(params): Parameters<CreateEventRequest>,
1307    ) -> Result<Json<EventOutput>, McpError> {
1308        let _permit = self.concurrency.acquire().await.unwrap();
1309        let manager = EventsManager::new();
1310
1311        let start = match parse_datetime(&params.start) {
1312            Ok(dt) => dt,
1313            Err(e) => return Err(mcp_invalid(format!("Error: {e}"))),
1314        };
1315
1316        let end = if let Some(end_str) = &params.end {
1317            match parse_datetime(end_str) {
1318                Ok(dt) => dt,
1319                Err(e) => return Err(mcp_invalid(format!("Error: {e}"))),
1320            }
1321        } else {
1322            start + Duration::minutes(params.duration_minutes)
1323        };
1324
1325        let calendar_id = if let Some(cal_name) = &params.calendar_name {
1326            match manager.list_calendars() {
1327                Ok(calendars) => calendars
1328                    .iter()
1329                    .find(|c| &c.title == cal_name)
1330                    .map(|c| c.identifier.clone()),
1331                Err(_) => None,
1332            }
1333        } else {
1334            None
1335        };
1336
1337        match manager.create_event(
1338            &params.title,
1339            start,
1340            end,
1341            params.notes.as_deref(),
1342            params.location.as_deref(),
1343            calendar_id.as_deref(),
1344            params.all_day,
1345        ) {
1346            Ok(event) => {
1347                let id = event.identifier.clone();
1348                if let Some(url) = &params.url {
1349                    let _ = manager.set_event_url(&id, Some(url));
1350                }
1351                if let Some(alarms) = &params.alarms {
1352                    apply_alarms_event(&manager, &id, alarms);
1353                }
1354                if let Some(recurrence) = &params.recurrence
1355                    && let Ok(rule) = parse_recurrence_param(recurrence)
1356                {
1357                    let _ = manager.set_event_recurrence_rule(&id, &rule);
1358                }
1359                let updated = manager.get_event(&id).unwrap_or(event);
1360                Ok(Json(EventOutput::from_item(&updated, &manager)))
1361            }
1362            Err(e) => Err(mcp_err(&e)),
1363        }
1364    }
1365
1366    #[tool(description = "Delete a calendar event from macOS Calendar.")]
1367    async fn delete_event(
1368        &self,
1369        Parameters(params): Parameters<EventIdRequest>,
1370    ) -> Result<Json<DeletedResponse>, McpError> {
1371        let _permit = self.concurrency.acquire().await.unwrap();
1372        let manager = EventsManager::new();
1373        match manager.delete_event(&params.event_id, params.affect_future) {
1374            Ok(_) => Ok(Json(DeletedResponse {
1375                id: params.event_id,
1376            })),
1377            Err(e) => Err(mcp_err(&e)),
1378        }
1379    }
1380
1381    #[tool(description = "Get a single calendar event by its unique identifier.")]
1382    async fn get_event(
1383        &self,
1384        Parameters(params): Parameters<EventIdRequest>,
1385    ) -> Result<Json<EventOutput>, McpError> {
1386        let _permit = self.concurrency.acquire().await.unwrap();
1387        let manager = EventsManager::new();
1388        match manager.get_event(&params.event_id) {
1389            Ok(e) => Ok(Json(EventOutput::from_item(&e, &manager))),
1390            Err(e) => Err(mcp_err(&e)),
1391        }
1392    }
1393
1394    // ========================================================================
1395    // Event Calendar Management
1396    // ========================================================================
1397
1398    #[tool(description = "Create a new calendar for events.")]
1399    async fn create_event_calendar(
1400        &self,
1401        Parameters(params): Parameters<CreateReminderListRequest>,
1402    ) -> Result<Json<CalendarOutput>, McpError> {
1403        let _permit = self.concurrency.acquire().await.unwrap();
1404        let manager = EventsManager::new();
1405        match manager.create_event_calendar(&params.name) {
1406            Ok(cal) => Ok(Json(CalendarOutput::from_info(&cal))),
1407            Err(e) => Err(mcp_err(&e)),
1408        }
1409    }
1410
1411    #[tool(description = "Update an event calendar — change name and/or color.")]
1412    async fn update_event_calendar(
1413        &self,
1414        Parameters(params): Parameters<UpdateEventCalendarRequest>,
1415    ) -> Result<Json<CalendarOutput>, McpError> {
1416        let _permit = self.concurrency.acquire().await.unwrap();
1417        let manager = EventsManager::new();
1418
1419        let color_rgba = params.color.as_ref().map(CalendarColor::to_rgba);
1420
1421        match manager.update_event_calendar(&params.calendar_id, params.name.as_deref(), color_rgba)
1422        {
1423            Ok(cal) => Ok(Json(CalendarOutput::from_info(&cal))),
1424            Err(e) => Err(mcp_err(&e)),
1425        }
1426    }
1427
1428    #[tool(
1429        description = "Delete an event calendar. WARNING: This will delete all events in the calendar!"
1430    )]
1431    async fn delete_event_calendar(
1432        &self,
1433        Parameters(params): Parameters<DeleteReminderListRequest>,
1434    ) -> Result<Json<DeletedResponse>, McpError> {
1435        let _permit = self.concurrency.acquire().await.unwrap();
1436        let manager = EventsManager::new();
1437        match manager.delete_event_calendar(&params.list_id) {
1438            Ok(()) => Ok(Json(DeletedResponse { id: params.list_id })),
1439            Err(e) => Err(mcp_err(&e)),
1440        }
1441    }
1442
1443    // ========================================================================
1444    // Sources
1445    // ========================================================================
1446
1447    #[tool(description = "List all available sources (accounts like iCloud, Local, Exchange).")]
1448    async fn list_sources(&self) -> Result<Json<ListResponse<SourceOutput>>, McpError> {
1449        let _permit = self.concurrency.acquire().await.unwrap();
1450        let manager = RemindersManager::new();
1451        match manager.list_sources() {
1452            Ok(sources) => {
1453                let items: Vec<_> = sources.iter().map(SourceOutput::from_info).collect();
1454                Ok(Json(ListResponse {
1455                    count: items.len(),
1456                    items,
1457                }))
1458            }
1459            Err(e) => Err(mcp_err(&e)),
1460        }
1461    }
1462
1463    // ========================================================================
1464    // Event Update Tool
1465    // ========================================================================
1466
1467    #[tool(
1468        description = "Update an existing calendar event. All fields are optional. Can update alarms, recurrence, and URL inline."
1469    )]
1470    async fn update_event(
1471        &self,
1472        Parameters(params): Parameters<UpdateEventRequest>,
1473    ) -> Result<Json<EventOutput>, McpError> {
1474        let _permit = self.concurrency.acquire().await.unwrap();
1475        let manager = EventsManager::new();
1476
1477        let start = match params.start.as_ref().map(|s| parse_datetime(s)).transpose() {
1478            Ok(v) => v,
1479            Err(e) => return Err(mcp_invalid(format!("Error: {e}"))),
1480        };
1481        let end = match params.end.as_ref().map(|s| parse_datetime(s)).transpose() {
1482            Ok(v) => v,
1483            Err(e) => return Err(mcp_invalid(format!("Error: {e}"))),
1484        };
1485
1486        match manager.update_event(
1487            &params.event_id,
1488            params.title.as_deref(),
1489            params.notes.as_deref(),
1490            params.location.as_deref(),
1491            start,
1492            end,
1493        ) {
1494            Ok(event) => {
1495                let id = event.identifier.clone();
1496                if let Some(url) = &params.url {
1497                    let url_val = if url.is_empty() {
1498                        None
1499                    } else {
1500                        Some(url.as_str())
1501                    };
1502                    let _ = manager.set_event_url(&id, url_val);
1503                }
1504                if let Some(alarms) = &params.alarms {
1505                    apply_alarms_event(&manager, &id, alarms);
1506                }
1507                if let Some(recurrence) = &params.recurrence {
1508                    if recurrence.frequency.is_empty() {
1509                        let _ = manager.remove_event_recurrence_rules(&id);
1510                    } else if let Ok(rule) = parse_recurrence_param(recurrence) {
1511                        let _ = manager.set_event_recurrence_rule(&id, &rule);
1512                    }
1513                }
1514                let updated = manager.get_event(&id).unwrap_or(event);
1515                Ok(Json(EventOutput::from_item(&updated, &manager)))
1516            }
1517            Err(e) => Err(mcp_err(&e)),
1518        }
1519    }
1520
1521    #[cfg(feature = "location")]
1522    #[tool(
1523        description = "Get the user's current location (latitude, longitude). Requires location permission."
1524    )]
1525    async fn get_current_location(&self) -> Result<Json<CoordinateOutput>, McpError> {
1526        let _permit = self.concurrency.acquire().await.unwrap();
1527        let manager = crate::location::LocationManager::new();
1528        match manager.get_current_location(std::time::Duration::from_secs(10)) {
1529            Ok(coord) => Ok(Json(CoordinateOutput {
1530                latitude: coord.latitude,
1531                longitude: coord.longitude,
1532            })),
1533            Err(e) => Err(McpError::internal_error(e.to_string(), None)),
1534        }
1535    }
1536    // ========================================================================
1537    // Search Tools
1538    // ========================================================================
1539
1540    #[tool(
1541        description = "Search reminders or events by text in title or notes (case-insensitive). Specify item_type to filter, or omit to search both."
1542    )]
1543    async fn search(
1544        &self,
1545        Parameters(params): Parameters<SearchRequest>,
1546    ) -> Result<Json<SearchResponse>, McpError> {
1547        let _permit = self.concurrency.acquire().await.unwrap();
1548        let query = params.query.to_lowercase();
1549
1550        let search_reminders = matches!(params.item_type, None | Some(ItemType::Reminder));
1551        let search_events = matches!(params.item_type, None | Some(ItemType::Event));
1552
1553        let reminders = if search_reminders {
1554            let manager = RemindersManager::new();
1555            let items = if params.include_completed {
1556                manager.fetch_all_reminders()
1557            } else {
1558                manager.fetch_incomplete_reminders()
1559            };
1560            items.ok().map(|items| {
1561                let filtered: Vec<_> = items
1562                    .into_iter()
1563                    .filter(|r| {
1564                        r.title.to_lowercase().contains(&query)
1565                            || r.notes
1566                                .as_deref()
1567                                .is_some_and(|n| n.to_lowercase().contains(&query))
1568                    })
1569                    .map(|r| ReminderOutput::from_item_summary(&r))
1570                    .collect();
1571                ListResponse {
1572                    count: filtered.len(),
1573                    items: filtered,
1574                }
1575            })
1576        } else {
1577            None
1578        };
1579
1580        let events = if search_events {
1581            let manager = EventsManager::new();
1582            let start = Local::now();
1583            let end = start + Duration::days(params.days);
1584            manager.fetch_events(start, end, None).ok().map(|items| {
1585                let filtered: Vec<_> = items
1586                    .into_iter()
1587                    .filter(|e| {
1588                        e.title.to_lowercase().contains(&query)
1589                            || e.notes
1590                                .as_deref()
1591                                .is_some_and(|n| n.to_lowercase().contains(&query))
1592                    })
1593                    .map(|e| EventOutput::from_item_summary(&e))
1594                    .collect();
1595                ListResponse {
1596                    count: filtered.len(),
1597                    items: filtered,
1598                }
1599            })
1600        } else {
1601            None
1602        };
1603
1604        Ok(Json(SearchResponse {
1605            query: params.query,
1606            reminders,
1607            events,
1608        }))
1609    }
1610
1611    // ========================================================================
1612    // Batch Operations
1613    // ========================================================================
1614
1615    #[tool(description = "Delete multiple reminders or events at once.")]
1616    async fn batch_delete(
1617        &self,
1618        Parameters(params): Parameters<BatchDeleteRequest>,
1619    ) -> Result<Json<BatchResponse>, McpError> {
1620        let _permit = self.concurrency.acquire().await.unwrap();
1621        let mut succeeded = 0usize;
1622        let mut errors = Vec::new();
1623
1624        match params.item_type {
1625            ItemType::Reminder => {
1626                let manager = RemindersManager::new();
1627                for id in &params.item_ids {
1628                    match manager.delete_reminder(id) {
1629                        Ok(_) => succeeded += 1,
1630                        Err(e) => errors.push(format!("{id}: {e}")),
1631                    }
1632                }
1633            }
1634            ItemType::Event => {
1635                let manager = EventsManager::new();
1636                for id in &params.item_ids {
1637                    match manager.delete_event(id, params.affect_future) {
1638                        Ok(_) => succeeded += 1,
1639                        Err(e) => errors.push(format!("{id}: {e}")),
1640                    }
1641                }
1642            }
1643        }
1644
1645        let err_items: Vec<_> = errors
1646            .into_iter()
1647            .map(|e| {
1648                let (id, msg) = e.split_once(": ").unwrap_or(("unknown", &e));
1649                BatchItemError {
1650                    item_id: id.to_string(),
1651                    message: msg.to_string(),
1652                }
1653            })
1654            .collect();
1655        Ok(Json(BatchResponse {
1656            total: params.item_ids.len(),
1657            succeeded,
1658            failed: err_items.len(),
1659            errors: err_items,
1660        }))
1661    }
1662
1663    #[tool(description = "Move multiple reminders to a different list at once.")]
1664    async fn batch_move(
1665        &self,
1666        Parameters(params): Parameters<BatchMoveRequest>,
1667    ) -> Result<Json<BatchResponse>, McpError> {
1668        let _permit = self.concurrency.acquire().await.unwrap();
1669        let manager = RemindersManager::new();
1670        let mut succeeded = 0usize;
1671        let mut errors = Vec::new();
1672
1673        for id in &params.reminder_ids {
1674            match manager.update_reminder(
1675                id,
1676                None,
1677                None,
1678                None,
1679                None,
1680                None,
1681                None,
1682                Some(&params.destination_list_name),
1683            ) {
1684                Ok(_) => succeeded += 1,
1685                Err(e) => errors.push(format!("{id}: {e}")),
1686            }
1687        }
1688
1689        let err_items: Vec<_> = errors
1690            .into_iter()
1691            .map(|e| {
1692                let (id, msg) = e.split_once(": ").unwrap_or(("unknown", &e));
1693                BatchItemError {
1694                    item_id: id.to_string(),
1695                    message: msg.to_string(),
1696                }
1697            })
1698            .collect();
1699        Ok(Json(BatchResponse {
1700            total: params.reminder_ids.len(),
1701            succeeded,
1702            failed: err_items.len(),
1703            errors: err_items,
1704        }))
1705    }
1706
1707    #[tool(description = "Update multiple reminders or events at once.")]
1708    async fn batch_update(
1709        &self,
1710        Parameters(params): Parameters<BatchUpdateRequest>,
1711    ) -> Result<Json<BatchResponse>, McpError> {
1712        let _permit = self.concurrency.acquire().await.unwrap();
1713        let mut succeeded = 0usize;
1714        let mut errors = Vec::new();
1715
1716        match params.item_type {
1717            ItemType::Reminder => {
1718                let manager = RemindersManager::new();
1719                for item in &params.updates {
1720                    let priority = item.priority.as_ref().map(Priority::to_usize);
1721                    let due_date = match &item.due_date {
1722                        Some(s) if s.is_empty() => Some(None),
1723                        Some(s) => match parse_datetime_or_time(s) {
1724                            Ok(dt) => Some(Some(dt)),
1725                            Err(e) => {
1726                                errors.push(format!("{}: {e}", item.item_id));
1727                                continue;
1728                            }
1729                        },
1730                        None => None,
1731                    };
1732                    match manager.update_reminder(
1733                        &item.item_id,
1734                        item.title.as_deref(),
1735                        item.notes.as_deref(),
1736                        item.completed,
1737                        priority,
1738                        due_date,
1739                        None,
1740                        None,
1741                    ) {
1742                        Ok(_) => succeeded += 1,
1743                        Err(e) => errors.push(format!("{}: {e}", item.item_id)),
1744                    }
1745                }
1746            }
1747            ItemType::Event => {
1748                let manager = EventsManager::new();
1749                for item in &params.updates {
1750                    match manager.update_event(
1751                        &item.item_id,
1752                        item.title.as_deref(),
1753                        item.notes.as_deref(),
1754                        None,
1755                        None,
1756                        None,
1757                    ) {
1758                        Ok(_) => succeeded += 1,
1759                        Err(e) => errors.push(format!("{}: {e}", item.item_id)),
1760                    }
1761                }
1762            }
1763        }
1764
1765        let total = params.updates.len();
1766        let err_items: Vec<_> = errors
1767            .into_iter()
1768            .map(|e| {
1769                let (id, msg) = e.split_once(": ").unwrap_or(("unknown", &e));
1770                BatchItemError {
1771                    item_id: id.to_string(),
1772                    message: msg.to_string(),
1773                }
1774            })
1775            .collect();
1776        Ok(Json(BatchResponse {
1777            total,
1778            succeeded,
1779            failed: err_items.len(),
1780            errors: err_items,
1781        }))
1782    }
1783}
1784
1785/// Parse a RecurrenceParam into a RecurrenceRule.
1786fn parse_recurrence_param(
1787    params: &RecurrenceParam,
1788) -> std::result::Result<crate::RecurrenceRule, String> {
1789    let frequency = match params.frequency.as_str() {
1790        "daily" => crate::RecurrenceFrequency::Daily,
1791        "weekly" => crate::RecurrenceFrequency::Weekly,
1792        "monthly" => crate::RecurrenceFrequency::Monthly,
1793        "yearly" => crate::RecurrenceFrequency::Yearly,
1794        other => {
1795            return Err(format!(
1796                "Invalid frequency: '{}'. Use daily, weekly, monthly, or yearly.",
1797                other
1798            ));
1799        }
1800    };
1801
1802    let end = if let Some(count) = params.end_after_count {
1803        crate::RecurrenceEndCondition::AfterCount(count)
1804    } else if let Some(date_str) = &params.end_date {
1805        let dt = parse_datetime(date_str)?;
1806        crate::RecurrenceEndCondition::OnDate(dt)
1807    } else {
1808        crate::RecurrenceEndCondition::Never
1809    };
1810
1811    Ok(crate::RecurrenceRule {
1812        frequency,
1813        interval: params.interval,
1814        end,
1815        days_of_week: params.days_of_week.clone(),
1816        days_of_month: params.days_of_month.clone(),
1817    })
1818}
1819
1820/// Parse a date/time string, defaulting to today if only time is given.
1821fn parse_datetime_or_time(s: &str) -> Result<DateTime<Local>, String> {
1822    // Try full datetime or date first
1823    if let Ok(dt) = parse_datetime(s) {
1824        return Ok(dt);
1825    }
1826    // Try time-only: "HH:MM" → use today's date
1827    if let Ok(time) = chrono::NaiveTime::parse_from_str(s, "%H:%M") {
1828        let today = Local::now().date_naive();
1829        let dt = today.and_time(time);
1830        return Local
1831            .from_local_datetime(&dt)
1832            .single()
1833            .ok_or_else(|| "Invalid local datetime".to_string());
1834    }
1835    Err(
1836        "Invalid date format. Use 'YYYY-MM-DD', 'YYYY-MM-DD HH:MM', or 'HH:MM' (uses today)"
1837            .to_string(),
1838    )
1839}
1840
1841/// Apply alarms to a reminder, clearing existing ones first.
1842fn apply_alarms_reminder(manager: &RemindersManager, id: &str, alarms: &[AlarmParam]) {
1843    // Clear existing alarms
1844    if let Ok(existing) = manager.get_alarms(id) {
1845        for i in (0..existing.len()).rev() {
1846            let _ = manager.remove_alarm(id, i);
1847        }
1848    }
1849    // Add new alarms
1850    for param in alarms {
1851        let alarm = alarm_param_to_info(param);
1852        let _ = manager.add_alarm(id, &alarm);
1853    }
1854}
1855
1856/// Apply alarms to an event, clearing existing ones first.
1857fn apply_alarms_event(manager: &EventsManager, id: &str, alarms: &[AlarmParam]) {
1858    if let Ok(existing) = manager.get_event_alarms(id) {
1859        for i in (0..existing.len()).rev() {
1860            let _ = manager.remove_event_alarm(id, i);
1861        }
1862    }
1863    for param in alarms {
1864        let alarm = alarm_param_to_info(param);
1865        let _ = manager.add_event_alarm(id, &alarm);
1866    }
1867}
1868
1869/// Convert an AlarmParam to an AlarmInfo.
1870fn alarm_param_to_info(param: &AlarmParam) -> crate::AlarmInfo {
1871    let proximity = match param.proximity.as_deref() {
1872        Some("enter") => crate::AlarmProximity::Enter,
1873        Some("leave") => crate::AlarmProximity::Leave,
1874        _ => crate::AlarmProximity::None,
1875    };
1876    let location = if let (Some(title), Some(lat), Some(lng)) =
1877        (&param.location_title, param.latitude, param.longitude)
1878    {
1879        Some(crate::StructuredLocation {
1880            title: title.clone(),
1881            latitude: lat,
1882            longitude: lng,
1883            radius: param.radius.unwrap_or(100.0),
1884        })
1885    } else {
1886        None
1887    };
1888    crate::AlarmInfo {
1889        relative_offset: param.relative_offset,
1890        absolute_date: None,
1891        proximity,
1892        location,
1893    }
1894}
1895
1896/// Format alarms for display output.
1897/// Extract #tags from notes content.
1898fn extract_tags(notes: &str) -> Vec<String> {
1899    notes
1900        .split_whitespace()
1901        .filter(|w| w.starts_with('#') && w.len() > 1)
1902        .map(|w| w[1..].to_string())
1903        .collect()
1904}
1905
1906/// Merge tags into notes. Removes existing #tag tokens, appends new ones.
1907fn apply_tags(notes: Option<&str>, tags: &[String]) -> String {
1908    // Keep lines that aren't purely tags
1909    let mut result: Vec<String> = notes
1910        .unwrap_or("")
1911        .lines()
1912        .filter(|line| {
1913            let trimmed = line.trim();
1914            // Remove lines that are only #tags
1915            !trimmed
1916                .split_whitespace()
1917                .all(|w| w.starts_with('#') && w.len() > 1)
1918                || trimmed.is_empty()
1919        })
1920        .map(String::from)
1921        .collect();
1922    // Remove trailing empty lines
1923    while result.last().is_some_and(std::string::String::is_empty) {
1924        result.pop();
1925    }
1926    if !tags.is_empty() {
1927        if !result.is_empty() {
1928            result.push(String::new());
1929        }
1930        result.push(
1931            tags.iter()
1932                .map(|t| format!("#{t}"))
1933                .collect::<Vec<_>>()
1934                .join(" "),
1935        );
1936    }
1937    result.join("\n")
1938}
1939
1940// ============================================================================
1941// Prompts
1942// ============================================================================
1943
1944#[prompt_router]
1945impl EventKitServer {
1946    /// List all incomplete (not yet finished) reminders, optionally filtered by list name.
1947    #[prompt(
1948        name = "incomplete_reminders",
1949        description = "List all incomplete reminders"
1950    )]
1951    async fn incomplete_reminders(
1952        &self,
1953        Parameters(args): Parameters<ListRemindersPromptArgs>,
1954    ) -> Result<GetPromptResult, McpError> {
1955        let _permit = self.concurrency.acquire().await.unwrap();
1956        let manager = RemindersManager::new();
1957        let reminders = manager.fetch_incomplete_reminders().map_err(|e| {
1958            McpError::internal_error(format!("Failed to list reminders: {e}"), None)
1959        })?;
1960
1961        // Filter by list name if provided
1962        let reminders: Vec<_> = if let Some(ref name) = args.list_name {
1963            reminders
1964                .into_iter()
1965                .filter(|r| r.calendar_title.as_deref() == Some(name.as_str()))
1966                .collect()
1967        } else {
1968            reminders
1969        };
1970
1971        let mut output = String::new();
1972        for r in &reminders {
1973            output.push_str(&format!(
1974                "- [{}] {} (id: {}){}{}\n",
1975                if r.completed { "x" } else { " " },
1976                r.title,
1977                r.identifier,
1978                r.due_date
1979                    .map(|d| format!(", due: {}", d.format("%Y-%m-%d %H:%M")))
1980                    .unwrap_or_default(),
1981                r.calendar_title
1982                    .as_ref()
1983                    .map(|l| format!(", list: {l}"))
1984                    .unwrap_or_default(),
1985            ));
1986        }
1987
1988        if output.is_empty() {
1989            output = "No incomplete reminders found.".to_string();
1990        }
1991
1992        Ok(GetPromptResult::new(vec![PromptMessage::new_text(
1993            PromptMessageRole::User,
1994            format!(
1995                "Here are the current incomplete reminders:\n\n{output}\n\nPlease help me manage these reminders."
1996            ),
1997        )])
1998        .with_description("Incomplete reminders"))
1999    }
2000
2001    /// List all reminder lists (calendars) available in macOS Reminders.
2002    #[prompt(
2003        name = "reminder_lists",
2004        description = "List all reminder lists available in Reminders"
2005    )]
2006    async fn reminder_lists_prompt(&self) -> Result<GetPromptResult, McpError> {
2007        let _permit = self.concurrency.acquire().await.unwrap();
2008        let manager = RemindersManager::new();
2009        let lists = manager.list_calendars().map_err(|e| {
2010            McpError::internal_error(format!("Failed to list calendars: {e}"), None)
2011        })?;
2012
2013        let mut output = String::new();
2014        for list in &lists {
2015            output.push_str(&format!("- {} (id: {})\n", list.title, list.identifier));
2016        }
2017
2018        if output.is_empty() {
2019            output = "No reminder lists found.".to_string();
2020        }
2021
2022        Ok(GetPromptResult::new(vec![PromptMessage::new_text(
2023            PromptMessageRole::User,
2024            format!(
2025                "Here are the available reminder lists:\n\n{output}\n\nWhich list would you like to work with?"
2026            ),
2027        )])
2028        .with_description("Available reminder lists"))
2029    }
2030
2031    /// Move a reminder to a different reminder list.
2032    #[prompt(
2033        name = "move_reminder",
2034        description = "Move a reminder to a different list"
2035    )]
2036    async fn move_reminder_prompt(
2037        &self,
2038        Parameters(args): Parameters<MoveReminderPromptArgs>,
2039    ) -> Result<GetPromptResult, McpError> {
2040        let _permit = self.concurrency.acquire().await.unwrap();
2041        let manager = RemindersManager::new();
2042
2043        // Find the destination calendar
2044        let lists = manager.list_calendars().map_err(|e| {
2045            McpError::internal_error(format!("Failed to list calendars: {e}"), None)
2046        })?;
2047
2048        let dest = lists.iter().find(|l| {
2049            l.title
2050                .to_lowercase()
2051                .contains(&args.destination_list.to_lowercase())
2052        });
2053
2054        match dest {
2055            Some(dest_list) => {
2056                match manager.update_reminder(
2057                    &args.reminder_id,
2058                    None,
2059                    None,
2060                    None,
2061                    None,
2062                    None,
2063                    None,
2064                    Some(&dest_list.title),
2065                ) {
2066                    Ok(updated) => Ok(GetPromptResult::new(vec![PromptMessage::new_text(
2067                        PromptMessageRole::User,
2068                        format!(
2069                            "Moved reminder \"{}\" to list \"{}\".",
2070                            updated.title, dest_list.title
2071                        ),
2072                    )])
2073                    .with_description("Reminder moved")),
2074                    Err(e) => Ok(GetPromptResult::new(vec![PromptMessage::new_text(
2075                        PromptMessageRole::User,
2076                        format!("Failed to move reminder: {e}"),
2077                    )])
2078                    .with_description("Move failed")),
2079                }
2080            }
2081            None => {
2082                let available: Vec<&str> = lists.iter().map(|l| l.title.as_str()).collect();
2083                Ok(GetPromptResult::new(vec![PromptMessage::new_text(
2084                    PromptMessageRole::User,
2085                    format!(
2086                        "Could not find reminder list \"{}\". Available lists: {}",
2087                        args.destination_list,
2088                        available.join(", ")
2089                    ),
2090                )])
2091                .with_description("List not found"))
2092            }
2093        }
2094    }
2095
2096    /// Create a new reminder with optional notes, priority, due date, and list.
2097    #[prompt(
2098        name = "create_detailed_reminder",
2099        description = "Create a reminder with detailed context like notes, priority, and due date"
2100    )]
2101    async fn create_detailed_reminder_prompt(
2102        &self,
2103        Parameters(args): Parameters<CreateReminderPromptArgs>,
2104    ) -> Result<GetPromptResult, McpError> {
2105        let _permit = self.concurrency.acquire().await.unwrap();
2106        let manager = RemindersManager::new();
2107
2108        let due = args
2109            .due_date
2110            .as_deref()
2111            .map(parse_datetime)
2112            .transpose()
2113            .map_err(|e| McpError::internal_error(format!("Invalid due date: {e}"), None))?;
2114
2115        match manager.create_reminder(
2116            &args.title,
2117            args.notes.as_deref(),
2118            args.list_name.as_deref(),
2119            args.priority.map(|p| p as usize),
2120            due,
2121            None,
2122        ) {
2123            Ok(reminder) => {
2124                let mut details = format!("Created reminder: \"{}\"", reminder.title);
2125                if let Some(notes) = &reminder.notes {
2126                    details.push_str(&format!("\nNotes: {notes}"));
2127                }
2128                if reminder.priority > 0 {
2129                    details.push_str(&format!("\nPriority: {}", reminder.priority));
2130                }
2131                if let Some(due) = &reminder.due_date {
2132                    details.push_str(&format!("\nDue: {}", due.format("%Y-%m-%d %H:%M")));
2133                }
2134                if let Some(list) = &reminder.calendar_title {
2135                    details.push_str(&format!("\nList: {list}"));
2136                }
2137
2138                Ok(GetPromptResult::new(vec![PromptMessage::new_text(
2139                    PromptMessageRole::User,
2140                    details,
2141                )])
2142                .with_description("Reminder created"))
2143            }
2144            Err(e) => Ok(GetPromptResult::new(vec![PromptMessage::new_text(
2145                PromptMessageRole::User,
2146                format!("Failed to create reminder: {e}"),
2147            )])
2148            .with_description("Creation failed")),
2149        }
2150    }
2151}
2152
2153// Implement the server handler
2154#[tool_handler]
2155#[prompt_handler]
2156impl rmcp::ServerHandler for EventKitServer {
2157    fn get_info(&self) -> ServerInfo {
2158        ServerInfo::new(
2159            ServerCapabilities::builder()
2160                .enable_tools()
2161                .enable_prompts()
2162                .build(),
2163        )
2164        .with_instructions(
2165            "This MCP server provides access to macOS Calendar events and Reminders. \
2166             Use the available tools to list, create, update, and delete calendar events \
2167             and reminders. Authorization is handled automatically on first use.",
2168        )
2169    }
2170}
2171
2172/// Serve the EventKit MCP server on any async read/write transport.
2173///
2174/// Used by the in-process gateway (via `DuplexStream`) and for testing.
2175/// The standalone binary uses [`run_mcp_server`] which wraps this with stdio.
2176pub async fn serve_on<T>(transport: T) -> anyhow::Result<()>
2177where
2178    T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
2179{
2180    let server = EventKitServer::new();
2181    let service = server.serve(transport).await?;
2182    service.waiting().await?;
2183    Ok(())
2184}
2185
2186/// Run the EventKit MCP server on stdio transport.
2187///
2188/// This initializes logging to stderr (MCP uses stdout/stdin for protocol)
2189/// and starts the MCP server. Used by the standalone binary (`eventkit --mcp`).
2190pub async fn run_mcp_server() -> anyhow::Result<()> {
2191    tracing_subscriber::fmt()
2192        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
2193        .with_writer(std::io::stderr)
2194        .init();
2195
2196    let server = EventKitServer::new();
2197    let service = server.serve(stdio()).await?;
2198    service.waiting().await?;
2199    Ok(())
2200}
2201
2202// ============================================================================
2203// Dump helpers — serialize objects to JSON for CLI debugging
2204// ============================================================================
2205
2206/// Dump a single reminder as pretty JSON (with alarms, recurrence, tags).
2207pub fn dump_reminder(id: &str) -> Result<String, crate::EventKitError> {
2208    let manager = RemindersManager::new();
2209    let r = manager.get_reminder(id)?;
2210    let output = ReminderOutput::from_item(&r, &manager);
2211    Ok(serde_json::to_string_pretty(&output).unwrap())
2212}
2213
2214/// Dump all reminders as pretty JSON (summary mode — no alarm/recurrence fetch).
2215pub fn dump_reminders(list_name: Option<&str>) -> Result<String, crate::EventKitError> {
2216    let manager = RemindersManager::new();
2217    let items = manager.fetch_all_reminders()?;
2218    let filtered: Vec<_> = if let Some(name) = list_name {
2219        items
2220            .into_iter()
2221            .filter(|r| r.calendar_title.as_deref() == Some(name))
2222            .collect()
2223    } else {
2224        items
2225    };
2226    let output: Vec<_> = filtered
2227        .iter()
2228        .map(ReminderOutput::from_item_summary)
2229        .collect();
2230    Ok(serde_json::to_string_pretty(&output).unwrap())
2231}
2232
2233/// Dump a single event as pretty JSON (with alarms, recurrence, attendees).
2234pub fn dump_event(id: &str) -> Result<String, crate::EventKitError> {
2235    let manager = EventsManager::new();
2236    let e = manager.get_event(id)?;
2237    let output = EventOutput::from_item(&e, &manager);
2238    Ok(serde_json::to_string_pretty(&output).unwrap())
2239}
2240
2241/// Dump upcoming events as pretty JSON.
2242pub fn dump_events(days: i64) -> Result<String, crate::EventKitError> {
2243    let manager = EventsManager::new();
2244    let items = manager.fetch_upcoming_events(days)?;
2245    let output: Vec<_> = items.iter().map(EventOutput::from_item_summary).collect();
2246    Ok(serde_json::to_string_pretty(&output).unwrap())
2247}
2248
2249/// Dump all reminder lists as pretty JSON.
2250pub fn dump_reminder_lists() -> Result<String, crate::EventKitError> {
2251    let manager = RemindersManager::new();
2252    let lists = manager.list_calendars()?;
2253    let output: Vec<_> = lists.iter().map(CalendarOutput::from_info).collect();
2254    Ok(serde_json::to_string_pretty(&output).unwrap())
2255}
2256
2257/// Dump all event calendars as pretty JSON.
2258pub fn dump_calendars() -> Result<String, crate::EventKitError> {
2259    let manager = EventsManager::new();
2260    let cals = manager.list_calendars()?;
2261    let output: Vec<_> = cals.iter().map(CalendarOutput::from_info).collect();
2262    Ok(serde_json::to_string_pretty(&output).unwrap())
2263}
2264
2265/// Dump all sources as pretty JSON.
2266pub fn dump_sources() -> Result<String, crate::EventKitError> {
2267    let manager = RemindersManager::new();
2268    let sources = manager.list_sources()?;
2269    let output: Vec<_> = sources.iter().map(SourceOutput::from_info).collect();
2270    Ok(serde_json::to_string_pretty(&output).unwrap())
2271}