Skip to main content

things_mcp/tools/
lists.rs

1//! Read tools that surface a Things list view.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::core::reader::queries::{list_areas, list_inbox, list_today, ListInboxParams, ListTodayParams};
7use crate::core::types::{AreaList, ProjectList, TodoList};
8use crate::core::reader::queries::{list_upcoming, ListUpcomingParams};
9use crate::core::reader::queries::{list_anytime, ListAnytimeParams};
10use crate::core::reader::queries::{list_someday, ListSomedayParams};
11use crate::core::reader::queries::{list_logbook, ListLogbookParams};
12use crate::core::reader::queries::{list_trash, ListTrashParams};
13use crate::state::AppState;
14
15#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
16pub struct ListInboxArgs {
17    /// Cap on returned rows. Defaults to 200.
18    #[serde(default)]
19    pub limit: Option<u32>,
20    /// If true, completed inbox to-dos are also returned. Defaults to false.
21    #[serde(default)]
22    pub include_completed: Option<bool>,
23}
24
25pub async fn things_list_inbox(
26    state: AppState,
27    args: ListInboxArgs,
28) -> anyhow::Result<TodoList> {
29    let params = ListInboxParams {
30        include_completed: args.include_completed.unwrap_or(false),
31        limit: args.limit.unwrap_or(200),
32    };
33    let items = list_inbox(&state.pool, params).await?;
34    Ok(TodoList { items })
35}
36
37#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
38pub struct ListTodayArgs {
39    /// Cap on returned rows. Defaults to 200.
40    #[serde(default)]
41    pub limit: Option<u32>,
42}
43
44pub async fn things_list_today(
45    state: AppState,
46    args: ListTodayArgs,
47) -> anyhow::Result<TodoList> {
48    let params = ListTodayParams {
49        limit: args.limit.unwrap_or(200),
50    };
51    let items = list_today(&state.pool, params).await?;
52    Ok(TodoList { items })
53}
54
55#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
56pub struct ListUpcomingArgs {
57    /// Lower bound (exclusive) as `YYYY-MM-DD`. Defaults to today.
58    #[serde(default)]
59    pub from: Option<String>,
60    /// Upper bound (inclusive) as `YYYY-MM-DD`. If omitted, no upper bound.
61    #[serde(default)]
62    pub to: Option<String>,
63    /// Cap on returned rows. Defaults to 200.
64    #[serde(default)]
65    pub limit: Option<u32>,
66}
67
68pub async fn things_list_upcoming(
69    state: AppState,
70    args: ListUpcomingArgs,
71) -> anyhow::Result<TodoList> {
72    let params = ListUpcomingParams {
73        from_iso: args.from,
74        to_iso: args.to,
75        limit: args.limit.unwrap_or(200),
76    };
77    let items = list_upcoming(&state.pool, params).await?;
78    Ok(TodoList { items })
79}
80
81#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
82pub struct ListAnytimeArgs {
83    /// Restrict to to-dos belonging to a specific area (directly or via project). Optional.
84    #[serde(default)]
85    pub area_id: Option<String>,
86    /// Cap on returned rows. Defaults to 200.
87    #[serde(default)]
88    pub limit: Option<u32>,
89}
90
91pub async fn things_list_anytime(
92    state: AppState,
93    args: ListAnytimeArgs,
94) -> anyhow::Result<TodoList> {
95    let params = ListAnytimeParams {
96        area_id: args.area_id,
97        limit: args.limit.unwrap_or(200),
98    };
99    let items = list_anytime(&state.pool, params).await?;
100    Ok(TodoList { items })
101}
102
103#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
104pub struct ListSomedayArgs {
105    /// Cap on returned rows. Defaults to 200.
106    #[serde(default)]
107    pub limit: Option<u32>,
108}
109
110pub async fn things_list_someday(
111    state: AppState,
112    args: ListSomedayArgs,
113) -> anyhow::Result<TodoList> {
114    let params = ListSomedayParams {
115        limit: args.limit.unwrap_or(200),
116    };
117    let items = list_someday(&state.pool, params).await?;
118    Ok(TodoList { items })
119}
120
121#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
122pub struct ListLogbookArgs {
123    /// Lower bound on completion date as `YYYY-MM-DD` (inclusive). Optional.
124    #[serde(default)]
125    pub from: Option<String>,
126    /// Upper bound on completion date as `YYYY-MM-DD` (inclusive — end-of-day). Optional.
127    #[serde(default)]
128    pub to: Option<String>,
129    /// Cap on returned rows. Defaults to 100.
130    #[serde(default)]
131    pub limit: Option<u32>,
132}
133
134pub async fn things_list_logbook(
135    state: AppState,
136    args: ListLogbookArgs,
137) -> anyhow::Result<TodoList> {
138    let params = ListLogbookParams {
139        from_iso: args.from,
140        to_iso: args.to,
141        limit: args.limit.unwrap_or(100),
142    };
143    let items = list_logbook(&state.pool, params).await?;
144    Ok(TodoList { items })
145}
146
147#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
148pub struct ListTrashArgs {
149    /// Cap on returned rows. Defaults to 100.
150    #[serde(default)]
151    pub limit: Option<u32>,
152}
153
154pub async fn things_list_trash(
155    state: AppState,
156    args: ListTrashArgs,
157) -> anyhow::Result<TodoList> {
158    let params = ListTrashParams {
159        limit: args.limit.unwrap_or(100),
160    };
161    let items = list_trash(&state.pool, params).await?;
162    Ok(TodoList { items })
163}
164
165#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
166pub struct ListAreasArgs {}
167
168pub async fn things_list_areas(
169    state: AppState,
170    _args: ListAreasArgs,
171) -> anyhow::Result<AreaList> {
172    let items = list_areas(&state.pool).await?;
173    Ok(AreaList { items })
174}
175
176use crate::core::reader::queries::{list_projects, ListProjectsParams, ProjectStatusFilter};
177use crate::core::reader::queries::{list_by_tag, ListByTagParams};
178
179#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, Default)]
180#[serde(rename_all = "snake_case")]
181pub enum ProjectStatusArg {
182    #[default]
183    Open,
184    Done,
185    All,
186}
187
188impl From<ProjectStatusArg> for ProjectStatusFilter {
189    fn from(a: ProjectStatusArg) -> Self {
190        match a {
191            ProjectStatusArg::Open => Self::Open,
192            ProjectStatusArg::Done => Self::Done,
193            ProjectStatusArg::All => Self::All,
194        }
195    }
196}
197
198#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
199pub struct ListProjectsArgs {
200    /// Restrict to projects in a given area. Optional.
201    #[serde(default)]
202    pub area_id: Option<String>,
203    /// `open` (default), `done`, or `all`.
204    #[serde(default)]
205    pub status: Option<ProjectStatusArg>,
206}
207
208pub async fn things_list_projects(
209    state: AppState,
210    args: ListProjectsArgs,
211) -> anyhow::Result<ProjectList> {
212    let params = ListProjectsParams {
213        area_id: args.area_id,
214        status: args.status.unwrap_or_default().into(),
215    };
216    let items = list_projects(&state.pool, params).await?;
217    Ok(ProjectList { items })
218}
219
220#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
221pub struct ListByTagArgs {
222    /// Tag identifier — either the user-facing title (`"Errand"`) or the UUID (`"tag-errand"`).
223    pub tag: String,
224    /// If true (default), also matches descendants of the named tag.
225    #[serde(default)]
226    pub recurse: Option<bool>,
227    /// Cap on returned rows. Defaults to 200.
228    #[serde(default)]
229    pub limit: Option<u32>,
230}
231
232pub async fn things_list_by_tag(
233    state: AppState,
234    args: ListByTagArgs,
235) -> anyhow::Result<TodoList> {
236    let params = ListByTagParams {
237        tag: args.tag,
238        recurse: args.recurse.unwrap_or(true),
239        limit: args.limit.unwrap_or(200),
240    };
241    let items = list_by_tag(&state.pool, params).await?;
242    Ok(TodoList { items })
243}