things_mcp/tools/
projects.rs1use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::core::reader::queries::get_project;
7use crate::core::types::MaybeProject;
8use crate::state::AppState;
9
10#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
11pub struct GetProjectArgs {
12 pub id: String,
14}
15
16pub async fn things_get_project(
17 state: AppState,
18 args: GetProjectArgs,
19) -> anyhow::Result<MaybeProject> {
20 let project = get_project(&state.pool, args.id).await?;
21 Ok(MaybeProject { project })
22}
23
24use std::time::{SystemTime, UNIX_EPOCH};
25
26use crate::core::writer::operation::{AddProjectSpec, Operation, UpdateProjectSpec};
27use crate::core::writer::outcome::WriteOutcome;
28use crate::core::writer::verify::VerifyPredicate;
29
30#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
31pub struct AddProjectArgs {
32 pub title: String,
34 #[serde(default)]
35 pub notes: Option<String>,
36 #[serde(default)]
37 pub when: Option<String>,
38 #[serde(default)]
39 pub deadline: Option<String>,
40 #[serde(default)]
41 pub tags: Vec<String>,
42 #[serde(default)]
44 pub area_id: Option<String>,
45 #[serde(default)]
47 pub headings: Vec<String>,
48}
49
50pub async fn things_add_project(
51 state: AppState,
52 args: AddProjectArgs,
53) -> anyhow::Result<WriteOutcome> {
54 if args.title.trim().is_empty() {
55 return Err(crate::core::error::ThingsError::InvalidInput {
56 field: "title".into(),
57 reason: "title must be non-empty".into(),
58 }
59 .into());
60 }
61 let since_unix = SystemTime::now()
62 .duration_since(UNIX_EPOCH)
63 .map(|d| d.as_secs_f64())
64 .unwrap_or(0.0);
65 let op = Operation::AddProject(AddProjectSpec {
66 title: args.title.clone(),
67 notes: args.notes,
68 when: args.when,
69 deadline: args.deadline,
70 tags: args.tags,
71 area_id: args.area_id,
72 todos: Vec::new(),
73 headings: args.headings,
74 });
75 let predicate = VerifyPredicate::CreateByTitle {
76 title: args.title,
77 since_unix,
78 kind: crate::core::types::TaskKind::Project,
79 };
80 let outcome = state.writer.fire(op, Some(predicate)).await?;
81 Ok(outcome)
82}
83
84#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
85pub struct UpdateProjectArgs {
86 pub id: String,
88 #[serde(default)]
89 pub title: Option<String>,
90 #[serde(default)]
91 pub notes: Option<String>,
92 #[serde(default)]
93 pub when: Option<String>,
94 #[serde(default)]
95 pub deadline: Option<String>,
96 #[serde(default)]
98 pub tags: Option<Vec<String>>,
99 #[serde(default)]
100 pub area_id: Option<String>,
101 #[serde(default)]
102 pub completed: Option<bool>,
103 #[serde(default)]
104 pub canceled: Option<bool>,
105}
106
107pub async fn things_update_project(
108 state: AppState,
109 args: UpdateProjectArgs,
110) -> anyhow::Result<WriteOutcome> {
111 if args.id.trim().is_empty() {
112 return Err(crate::core::error::ThingsError::InvalidInput {
113 field: "id".into(),
114 reason: "id must be non-empty".into(),
115 }
116 .into());
117 }
118 let op = Operation::UpdateProject(UpdateProjectSpec {
119 id: args.id.clone(),
120 title: args.title.clone(),
121 notes: args.notes.clone(),
122 when: args.when,
123 deadline: args.deadline,
124 tags: args.tags,
125 area_id: args.area_id,
126 completed: args.completed,
127 canceled: args.canceled,
128 });
129 let predicate = VerifyPredicate::UpdateById {
130 id: args.id,
131 expected_title: args.title,
132 expected_notes: args.notes,
133 };
134 let outcome = state.writer.fire(op, Some(predicate)).await?;
135 Ok(outcome)
136}