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