1use rmcp::{
2 ErrorData, Json, handler::server::wrapper::Parameters, service::RequestContext, tool,
3 tool_router,
4};
5use schemars::JsonSchema;
6use serde::Deserialize;
7
8use super::{Bus, auth_of};
9use crate::{
10 model::{ClaimResult, TaskDetail, TaskInfo, TaskList},
11 store::tasks,
12};
13
14fn default_limit() -> i64 {
15 50
16}
17
18#[derive(Debug, Deserialize, JsonSchema)]
19pub struct CreateTaskArgs {
20 pub key: String,
23 pub title: String,
25 #[serde(default)]
28 pub description: Option<String>,
29 #[serde(default)]
31 #[schemars(schema_with = "crate::model::any_json_schema")]
32 pub metadata: Option<serde_json::Value>,
33 #[serde(default)]
36 pub depends_on: Vec<String>,
37}
38
39#[derive(Debug, Deserialize, JsonSchema)]
40pub struct ListTasksArgs {
41 #[serde(default)]
45 pub status: Option<String>,
46 #[serde(default)]
48 pub mine_only: bool,
49 #[serde(default = "default_limit")]
51 pub limit: i64,
52}
53
54#[derive(Debug, Deserialize, JsonSchema)]
55pub struct TaskKeyArgs {
56 pub key: String,
58}
59
60#[derive(Debug, Deserialize, JsonSchema)]
61pub struct ClaimTaskArgs {
62 pub key: String,
64 #[serde(default)]
67 pub lease_seconds: Option<i64>,
68}
69
70#[derive(Debug, Deserialize, JsonSchema)]
71pub struct ClaimNextArgs {
72 #[serde(default)]
74 pub lease_seconds: Option<i64>,
75}
76
77#[derive(Debug, Deserialize, JsonSchema)]
78pub struct CompleteTaskArgs {
79 pub key: String,
81 #[serde(default)]
84 pub result: Option<String>,
85}
86
87#[tool_router(router = tasks_router, vis = "pub")]
88impl Bus {
89 #[tool(
90 description = "Register a unit of shared work so the team can coordinate on it. \
91 Creating a task does not claim it. Use depends_on to chain work \
92 into a pipeline."
93 )]
94 async fn create_task(
95 &self,
96 ctx: RequestContext<rmcp::RoleServer>,
97 Parameters(args): Parameters<CreateTaskArgs>,
98 ) -> Result<Json<TaskInfo>, ErrorData> {
99 let auth = auth_of(&ctx)?;
100 let input = tasks::CreateInput {
101 key: args.key,
102 title: args.title,
103 description: args.description,
104 metadata: args.metadata,
105 depends_on: args.depends_on,
106 };
107 Ok(Json(tasks::create_task(&self.db, &auth, input).await?))
108 }
109
110 #[tool(
111 description = "List the team's tasks with who holds each one. Check this before \
112 starting work so you do not duplicate a teammate's effort."
113 )]
114 async fn list_tasks(
115 &self,
116 ctx: RequestContext<rmcp::RoleServer>,
117 Parameters(args): Parameters<ListTasksArgs>,
118 ) -> Result<Json<TaskList>, ErrorData> {
119 let auth = auth_of(&ctx)?;
120 Ok(Json(
121 tasks::list_tasks(&self.db, &auth, args.status, args.mine_only, args.limit).await?,
122 ))
123 }
124
125 #[tool(description = "Get one task with its full history of claims and completions.")]
126 async fn get_task(
127 &self,
128 ctx: RequestContext<rmcp::RoleServer>,
129 Parameters(args): Parameters<TaskKeyArgs>,
130 ) -> Result<Json<TaskDetail>, ErrorData> {
131 let auth = auth_of(&ctx)?;
132 Ok(Json(tasks::get_task(&self.db, &auth, &args.key).await?))
133 }
134
135 #[tool(
136 description = "Take exclusive ownership of a task before working on it. Fails \
137 cleanly (claimed=false) if a teammate holds an unexpired claim. \
138 Re-claiming a task you already hold extends your lease."
139 )]
140 async fn claim_task(
141 &self,
142 ctx: RequestContext<rmcp::RoleServer>,
143 Parameters(args): Parameters<ClaimTaskArgs>,
144 ) -> Result<Json<ClaimResult>, ErrorData> {
145 let auth = auth_of(&ctx)?;
146 Ok(Json(
147 tasks::claim_task(&self.db, &auth, &args.key, args.lease_seconds).await?,
148 ))
149 }
150
151 #[tool(
152 description = "Claim the oldest available task without naming it. Safe to call \
153 concurrently from several agents: each gets a different task."
154 )]
155 async fn claim_next_task(
156 &self,
157 ctx: RequestContext<rmcp::RoleServer>,
158 Parameters(args): Parameters<ClaimNextArgs>,
159 ) -> Result<Json<ClaimResult>, ErrorData> {
160 let auth = auth_of(&ctx)?;
161 Ok(Json(
162 tasks::claim_next_task(&self.db, &auth, args.lease_seconds).await?,
163 ))
164 }
165
166 #[tool(
167 description = "Extend the lease on a task you hold. Call this periodically \
168 during long work so the claim does not lapse and get stolen."
169 )]
170 async fn renew_task_lease(
171 &self,
172 ctx: RequestContext<rmcp::RoleServer>,
173 Parameters(args): Parameters<ClaimTaskArgs>,
174 ) -> Result<Json<TaskInfo>, ErrorData> {
175 let auth = auth_of(&ctx)?;
176 Ok(Json(
177 tasks::renew_lease(&self.db, &auth, &args.key, args.lease_seconds).await?,
178 ))
179 }
180
181 #[tool(
182 description = "Give up a task you claimed without finishing it, returning it to \
183 the open pool for someone else."
184 )]
185 async fn release_task(
186 &self,
187 ctx: RequestContext<rmcp::RoleServer>,
188 Parameters(args): Parameters<TaskKeyArgs>,
189 ) -> Result<Json<TaskInfo>, ErrorData> {
190 let auth = auth_of(&ctx)?;
191 Ok(Json(tasks::release_task(&self.db, &auth, &args.key).await?))
192 }
193
194 #[tool(
195 description = "Mark a task as done and record what was done. Write the result as \
196 if a teammate's agent will read it with no other context."
197 )]
198 async fn complete_task(
199 &self,
200 ctx: RequestContext<rmcp::RoleServer>,
201 Parameters(args): Parameters<CompleteTaskArgs>,
202 ) -> Result<Json<TaskInfo>, ErrorData> {
203 let auth = auth_of(&ctx)?;
204 Ok(Json(
205 tasks::complete_task(&self.db, &auth, &args.key, args.result).await?,
206 ))
207 }
208}