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