gtd_mcp/lib.rs
1//! GTD MCP Server Library
2//!
3//! This library provides a Model Context Protocol (MCP) server for GTD (Getting Things Done)
4//! task management. It implements the GTD methodology with support for tasks, projects,
5//! and contexts, with automatic Git-based version control.
6//!
7//! # Architecture
8//!
9//! The library follows a 3-layer architecture:
10//! - **MCP Layer**: `GtdServerHandler` - Handles MCP protocol communication
11//! - **Domain Layer**: `gtd` module - Core GTD data models and business logic
12//! - **Persistence Layer**: `storage` module - File-based TOML storage with Git sync
13//!
14//! # Example
15//!
16//! ```no_run
17//! use gtd_mcp::GtdServerHandler;
18//! use anyhow::Result;
19//!
20//! #[tokio::main]
21//! async fn main() -> Result<()> {
22//! let handler = GtdServerHandler::new("gtd.toml", false)?;
23//! // Use handler with MCP server...
24//! Ok(())
25//! }
26//! ```
27
28pub mod formatting;
29pub mod git_ops;
30pub mod gtd;
31pub mod handlers;
32pub mod migration;
33pub mod storage;
34pub mod validation;
35
36use anyhow::Result;
37
38use mcp_attr::Result as McpResult;
39use mcp_attr::server::{McpServer, mcp_server};
40use std::sync::Mutex;
41
42// Re-export for integration tests (McpServer trait already in scope above)
43
44// Re-export commonly used types
45pub use git_ops::GitOps;
46pub use gtd::{GtdData, Nota, NotaStatus, local_date_today};
47pub use storage::Storage;
48
49/// MCP Server handler for GTD task management
50///
51/// Provides an MCP interface to GTD functionality including task management,
52/// project tracking, and context organization. All changes are automatically
53/// persisted to a TOML file and optionally synchronized with Git.
54pub struct GtdServerHandler {
55 pub data: Mutex<GtdData>,
56 pub storage: Storage,
57}
58
59impl GtdServerHandler {
60 /// Create a new GTD server handler
61 ///
62 /// # Arguments
63 /// * `storage_path` - Path to the GTD data file (TOML format)
64 /// * `sync_git` - Enable automatic Git synchronization
65 ///
66 /// # Returns
67 /// Result containing the handler or an error
68 ///
69 /// # Example
70 /// ```no_run
71 /// # use gtd_mcp::GtdServerHandler;
72 /// # use anyhow::Result;
73 /// # fn main() -> Result<()> {
74 /// let handler = GtdServerHandler::new("gtd.toml", false)?;
75 /// # Ok(())
76 /// # }
77 /// ```
78 pub fn new(storage_path: &str, sync_git: bool) -> Result<Self> {
79 let storage = Storage::new(storage_path, sync_git);
80 let data = Mutex::new(storage.load()?);
81 Ok(Self { data, storage })
82 }
83
84 /// Save GTD data with a default commit message.
85 ///
86 /// Persists the current in-memory GTD data to disk using the default commit message
87 /// defined in `Storage::save()`, which is "Update GTD data".
88 /// This is typically called by handler modules after modifying GTD data,
89 /// following the MCP tool implementation pattern.
90 pub fn save_data(&self) -> Result<()> {
91 let data = self.data.lock().unwrap();
92 self.storage.save(&data)?;
93 Ok(())
94 }
95
96 /// Save GTD data with a custom commit message.
97 ///
98 /// Persists the current GTD data to disk and creates a Git commit using the provided message.
99 ///
100 /// # Arguments
101 /// * `message` - Commit message to use for the Git version history.
102 pub(crate) fn save_data_with_message(&self, message: &str) -> Result<()> {
103 let data = self.data.lock().unwrap();
104 self.storage.save_with_message(&data, message)?;
105 Ok(())
106 }
107}
108
109impl Drop for GtdServerHandler {
110 fn drop(&mut self) {
111 // Push to git on shutdown if sync is enabled
112 if let Err(e) = self.storage.shutdown() {
113 eprintln!("Warning: Shutdown git sync failed: {}", e);
114 }
115 }
116}
117
118/// GTD task management server implementing David Allen's methodology.
119/// Workflow: Capture(inbox) → Review(list) → Clarify(update) → Organize(change_status) → Do → Purge(empty_trash)
120///
121/// **Statuses**: inbox(start) | next_action(ready) | waiting_for(blocked) | later(deferred) | calendar(dated) | someday(maybe) | done | reference | trash
122/// **Types**: task | project(multi-step) | context(@location)
123/// **IDs**: Use meaningful strings (e.g., "call-john", "website-redesign")
124#[mcp_server]
125impl McpServer for GtdServerHandler {
126 /// **Purge**: Permanently delete all trashed items. Run weekly.
127 /// **When**: Part of weekly review - trash items first with change_status, then purge.
128 /// **Safety**: Checks references to prevent broken links.
129 #[tool]
130 pub async fn empty_trash(&self) -> McpResult<String> {
131 self.handle_empty_trash().await
132 }
133
134 /// **Capture**: Quickly capture anything needing attention. First GTD step - all items start here.
135 /// **When**: Something crosses your mind? Capture immediately without thinking.
136 /// **Next**: Use list(status="inbox") to review, then update/change_status to organize.
137 ///
138 /// **ID Naming Guidelines**:
139 /// - Use kebab-case (lowercase with hyphens): "fix-io-button", "review-q3-sales"
140 /// - Start with verb when possible: "update-", "fix-", "create-", "review-"
141 /// - Keep concise but meaningful (3-5 words max)
142 /// - Use project prefix for clarity: "eci-fix-button", "fft-level-cloud"
143 /// - IDs are immutable - choose carefully as they cannot be changed later
144 #[allow(clippy::too_many_arguments)]
145 #[tool]
146 pub async fn inbox(
147 &self,
148 /// Unique string ID - follow kebab-case guidelines above (e.g., "call-john", "web-redesign")
149 id: String,
150 /// Brief description
151 title: String,
152 /// inbox | next_action | waiting_for | later | calendar | someday | done | reference | project | context | trash
153 status: String,
154 /// Optional: Parent project ID
155 project: Option<String>,
156 /// Optional: Where applies (e.g., "@home", "@office")
157 context: Option<String>,
158 /// Optional: Markdown notes
159 notes: Option<String>,
160 /// Optional: YYYY-MM-DD, required for calendar status
161 start_date: Option<String>,
162 /// Optional: Recurrence pattern - daily | weekly | monthly | yearly
163 recurrence: Option<String>,
164 /// Optional: Recurrence configuration
165 /// - weekly: weekday names (e.g., "Monday,Wednesday,Friday")
166 /// - monthly: day numbers (e.g., "1,15,25")
167 /// - yearly: month-day pairs (e.g., "1-1,12-25" for Jan 1 and Dec 25)
168 recurrence_config: Option<String>,
169 ) -> McpResult<String> {
170 self.handle_inbox(
171 id,
172 title,
173 status,
174 project,
175 context,
176 notes,
177 start_date,
178 recurrence,
179 recurrence_config,
180 )
181 .await
182 }
183
184 /// **Review**: List/filter all items. Essential for daily/weekly reviews.
185 /// **When**: Daily - check next_action. Weekly - review all. Use filters to focus.
186 /// **Filters**: No filter=all | status="inbox"=unprocessed | status="next_action"=ready | status="calendar"+date=today's tasks | keyword="text"=search | project="id"=by project | context="name"=by context.
187 #[tool]
188 pub async fn list(
189 &self,
190 /// Optional: Filter by status (inbox | next_action | waiting_for | later | calendar | someday | done | reference | project | context | trash)
191 status: Option<String>,
192 /// Optional: Date filter YYYY-MM-DD - For calendar, shows tasks with start_date <= this date
193 date: Option<String>,
194 /// Optional: True to exclude notes and reduce token usage
195 exclude_notes: Option<bool>,
196 /// Optional: Search keyword in id, title and notes (case-insensitive)
197 keyword: Option<String>,
198 /// Optional: Filter by project ID - use meaningful abbreviation (e.g., "website-redesign", "q1-budget")
199 project: Option<String>,
200 /// Optional: Filter by context name
201 context: Option<String>,
202 ) -> McpResult<String> {
203 self.handle_list(status, date, exclude_notes, keyword, project, context)
204 .await
205 }
206
207 /// **Clarify**: Update item details. Add context, notes, project links after capturing.
208 /// **When**: After inbox capture, clarify what it is, why it matters, what's needed.
209 /// **Tip**: Use ""(empty string) to clear optional fields.
210 /// **Note**: Item ID cannot be changed - IDs are immutable. To "rename", create new item and delete old one.
211 #[allow(clippy::too_many_arguments)]
212 #[tool]
213 pub async fn update(
214 &self,
215 /// Item ID to update (immutable - cannot be changed)
216 id: String,
217 /// Optional: New title
218 title: Option<String>,
219 /// Optional: New status (changes type if project/context)
220 status: Option<String>,
221 /// Optional: Project link, ""=clear
222 project: Option<String>,
223 /// Optional: Context tag, ""=clear
224 context: Option<String>,
225 /// Optional: Markdown notes, ""=clear
226 notes: Option<String>,
227 /// Optional: Start date YYYY-MM-DD, ""=clear
228 start_date: Option<String>,
229 ) -> McpResult<String> {
230 self.handle_update(id, title, status, project, context, notes, start_date)
231 .await
232 }
233
234 /// **Organize/Do**: Move items through workflow stages as you process them.
235 /// **When**: inbox→next_action(ready) | →waiting_for(blocked) | →done(complete) | →trash(discard).
236 /// **Tip**: Use change_status to trash before empty_trash to permanently delete.
237 /// **Batch**: Supports multiple IDs for efficient batch operations (e.g., weekly review).
238 #[tool]
239 pub async fn change_status(
240 &self,
241 /// Item IDs to change - format: ["#1", "#2", "#3"] for batch operations, or single ID for single item
242 ids: Vec<String>,
243 /// New status: inbox | next_action | waiting_for | later | calendar | someday | done | reference | project | context | trash
244 new_status: String,
245 /// Optional: Start date YYYY-MM-DD (required for calendar)
246 start_date: Option<String>,
247 ) -> McpResult<String> {
248 self.handle_change_status(ids, new_status, start_date).await
249 }
250}