Skip to main content

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}