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