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