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