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