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