1use chrono::{Local, NaiveDate, TimeZone};
2use org_core::{OrgModeError, Priority, org_mode::AgendaViewType};
3use rmcp::{
4 ErrorData as McpError,
5 handler::server::wrapper::Parameters,
6 model::{CallToolResult, Content, ErrorCode},
7 schemars, tool, tool_router,
8};
9
10use crate::core::OrgModeRouter;
11
12#[derive(Debug, schemars::JsonSchema, serde::Deserialize)]
13pub struct AgendaRequest {
14 #[schemars(
15 description = "Start date for agenda view in ISO 8601 format (YYYY-MM-DD, optional)"
16 )]
17 pub start_date: Option<String>,
18 #[schemars(description = "End date for agenda view in ISO 8601 format (YYYY-MM-DD, optional)")]
19 pub end_date: Option<String>,
20 #[schemars(
21 description = "Filter by TODO states (optional, e.g., ['TODO', 'DONE', 'IN_PROGRESS'])"
22 )]
23 pub todo_states: Option<Vec<String>>,
24 #[schemars(
25 description = "Filter results by tags (optional, matches any of the provided tags)"
26 )]
27 pub tags: Option<Vec<String>>,
28 #[schemars(description = "Filter by priority level (optional: A, B, or C)")]
29 pub priority: Option<String>,
30 #[schemars(description = "Maximum number of items to return (optional)")]
31 pub limit: Option<usize>,
32 #[schemars(
33 description = "View mode: 'list' for all tasks, 'view' for date-organized agenda (default: 'list')"
34 )]
35 pub mode: Option<String>,
36}
37
38#[tool_router(router = "tool_router_agenda", vis = "pub(crate)")]
39impl OrgModeRouter {
40 #[tool(
41 name = "org-agenda",
42 description = "Query agenda items (TODO/DONE tasks) with support for filtering by dates, states, tags, and priorities. Use 'list' mode to get all tasks, or 'view' mode for calendar-like agenda organized by scheduled/deadline dates.",
43 annotations(title = "org-agenda tool")
44 )]
45 async fn tool_agenda(
46 &self,
47 Parameters(AgendaRequest {
48 start_date,
49 end_date,
50 todo_states,
51 tags,
52 priority,
53 limit,
54 mode,
55 }): Parameters<AgendaRequest>,
56 ) -> Result<CallToolResult, McpError> {
57 let org_mode = self.org_mode.lock().await;
58
59 let mode_str = mode.as_deref().unwrap_or("list");
60
61 let priority_filter = if let Some(ref p) = priority {
62 match p.to_uppercase().as_str() {
63 "A" => Some(Priority::A),
64 "B" => Some(Priority::B),
65 "C" => Some(Priority::C),
66 _ => {
67 return Err(McpError {
68 code: ErrorCode::INVALID_PARAMS,
69 message: format!("Invalid priority '{p}'. Must be A, B, or C.").into(),
70 data: None,
71 });
72 }
73 }
74 } else {
75 None
76 };
77
78 match mode_str {
79 "list" => {
80 let tasks = org_mode.list_tasks(
81 todo_states.as_deref(),
82 tags.as_deref(),
83 priority_filter,
84 limit,
85 );
86
87 match tasks {
88 Ok(tasks) => match Content::json(tasks) {
89 Ok(serialized) => Ok(CallToolResult::success(vec![serialized])),
90 Err(e) => Err(McpError {
91 code: ErrorCode::INTERNAL_ERROR,
92 message: format!("Failed to serialize tasks: {e}").into(),
93 data: None,
94 }),
95 },
96 Err(e) => Err(Self::map_org_error(e)),
97 }
98 }
99 "view" => {
100 let agenda_view_type = match (start_date, end_date) {
101 (Some(start), Some(end)) => {
102 let from_result = NaiveDate::parse_from_str(&start, "%Y-%m-%d");
103 let to_result = NaiveDate::parse_from_str(&end, "%Y-%m-%d");
104
105 match (from_result, to_result) {
107 (Ok(from_date), Ok(to_date)) => {
108 let from_datetime = Local
109 .from_local_datetime(
110 &from_date.and_hms_opt(0, 0, 0).unwrap_or_default(),
111 )
112 .single()
113 .unwrap_or_else(Local::now);
114 let to_datetime = Local
115 .from_local_datetime(
116 &to_date.and_hms_opt(23, 59, 59).unwrap_or_default(),
117 )
118 .single()
119 .unwrap_or_else(Local::now);
120
121 AgendaViewType::Custom {
122 from: from_datetime,
123 to: to_datetime,
124 }
125 }
126 _ => AgendaViewType::default(),
127 }
128 }
129 _ => AgendaViewType::default(),
130 };
131
132 let view = org_mode.get_agenda_view(
133 agenda_view_type,
134 todo_states.as_deref(),
135 tags.as_deref(),
136 );
137
138 match view {
139 Ok(view) => match Content::json(view) {
140 Ok(serialized) => Ok(CallToolResult::success(vec![serialized])),
141 Err(e) => Err(McpError {
142 code: ErrorCode::INTERNAL_ERROR,
143 message: format!("Failed to serialize agenda view: {e}").into(),
144 data: None,
145 }),
146 },
147 Err(e) => Err(Self::map_org_error(e)),
148 }
149 }
150 _ => Err(McpError {
151 code: ErrorCode::INVALID_PARAMS,
152 message: format!("Invalid mode '{mode_str}'. Must be 'list' or 'view'.").into(),
153 data: None,
154 }),
155 }
156 }
157}
158
159impl OrgModeRouter {
160 fn map_org_error(e: OrgModeError) -> McpError {
161 let error_code = match &e {
162 OrgModeError::InvalidDirectory(_) => ErrorCode::INVALID_PARAMS,
163 OrgModeError::WalkError(_) => ErrorCode::INTERNAL_ERROR,
164 OrgModeError::IoError(_) => ErrorCode::INTERNAL_ERROR,
165 _ => ErrorCode::INTERNAL_ERROR,
166 };
167 McpError {
168 code: error_code,
169 message: format!("Agenda query failed: {e}").into(),
170 data: None,
171 }
172 }
173}