Skip to main content

org_mcp_server/tools/
org_agenda.rs

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                        // TODO: improve error handling
106                        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}