use crate::store::ProjectStore;
use crate::types::*;
use adk_mcp_sdk::{HealthCheck, HealthStatus};
use chrono::NaiveDate;
use rmcp::{handler::server::wrapper::Parameters, schemars, tool, tool_router};
use serde::Deserialize;
use std::sync::Arc;
fn date(s: &Option<String>) -> Option<NaiveDate> {
s.as_ref().and_then(|x| NaiveDate::parse_from_str(x, "%Y-%m-%d").ok())
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct CreateProjectInput {
pub key: String,
pub name: String,
#[serde(default)]
pub description: String,
pub lead: Option<String>,
#[serde(default)]
pub members: Vec<String>,
pub start_date: Option<String>,
pub target_date: Option<String>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct IdInput { pub id: String }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ListProjectsInput { pub status: Option<ProjectStatus> }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SetProjectStatusInput { pub id: String, pub status: ProjectStatus }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct AddMemberInput { pub id: String, pub member: String }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct CreateTaskInput {
pub project_id: String,
#[serde(default = "default_task_type")]
pub task_type: TaskType,
pub title: String,
#[serde(default)]
pub description: String,
#[serde(default = "default_priority")]
pub priority: Priority,
pub reporter: String,
pub assignee: Option<String>,
pub parent_id: Option<String>,
pub estimate: Option<f64>,
pub due_date: Option<String>,
}
fn default_task_type() -> TaskType { TaskType::Task }
fn default_priority() -> Priority { Priority::Medium }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SearchTasksInput {
pub project_id: Option<String>,
pub query: Option<String>,
pub status: Option<TaskStatus>,
pub assignee: Option<String>,
pub task_type: Option<TaskType>,
pub sprint_id: Option<String>,
pub label: Option<String>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct UpdateTaskInput {
pub id: String,
pub title: Option<String>,
pub description: Option<String>,
pub priority: Option<Priority>,
pub assignee: Option<String>,
pub estimate: Option<f64>,
pub due_date: Option<String>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct TransitionTaskInput { pub id: String, pub status: TaskStatus }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct AssignTaskInput { pub id: String, pub assignee: Option<String> }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SetLabelsInput { pub id: String, pub labels: Vec<String> }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct DependencyInput { pub task_id: String, pub dep_type: DependencyType, pub other_id: String }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct CommentInput { pub task_id: String, pub author: String, pub body: String }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct LogTimeInput { pub task_id: String, pub user: String, pub hours: f64, pub note: Option<String> }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct CreateSprintInput {
pub project_id: String,
pub name: String,
pub goal: Option<String>,
pub start_date: Option<String>,
pub end_date: Option<String>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SetSprintStatusInput { pub id: String, pub status: SprintStatus }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct AssignSprintInput { pub task_id: String, pub sprint_id: Option<String> }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ProjectScopeInput { pub project_id: String }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct CreateMilestoneInput { pub project_id: String, pub name: String, #[serde(default)] pub description: String, pub due_date: Option<String> }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SetMilestoneStatusInput { pub id: String, pub status: MilestoneStatus }
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct AssignMilestoneInput { pub task_id: String, pub milestone_id: Option<String> }
#[derive(Clone)]
pub struct ProjectServer { pub store: Arc<ProjectStore> }
#[tool_router(server_handler)]
impl ProjectServer {
#[tool(description = "Create a project with a short KEY (used as the task-id prefix), name, lead, and members.")]
fn create_project(&self, Parameters(i): Parameters<CreateProjectInput>) -> String {
let p = self.store.create_project(i.key, i.name, i.description, i.lead, i.members, date(&i.start_date), date(&i.target_date));
serde_json::to_string_pretty(&serde_json::json!({"project_id": p.id, "key": p.key, "status": p.status})).unwrap()
}
#[tool(description = "Get a project's details.")]
fn get_project(&self, Parameters(i): Parameters<IdInput>) -> String {
match self.store.get_project(&i.id) {
Some(p) => serde_json::to_string_pretty(&p).unwrap(),
None => format!("Project not found: {}", i.id),
}
}
#[tool(description = "List projects, optionally filtered by status.")]
fn list_projects(&self, Parameters(i): Parameters<ListProjectsInput>) -> String {
let ps = self.store.list_projects(i.status);
let out: Vec<serde_json::Value> = ps.iter().map(|p| serde_json::json!({"id": p.id, "key": p.key, "name": p.name, "status": p.status, "lead": p.lead})).collect();
serde_json::to_string_pretty(&serde_json::json!({"count": out.len(), "projects": out})).unwrap()
}
#[tool(description = "Change a project's status (planning/active/on_hold/completed/archived). Gated.")]
fn set_project_status(&self, Parameters(i): Parameters<SetProjectStatusInput>) -> String {
match self.store.set_project_status(&i.id, i.status) {
Ok(p) => serde_json::to_string_pretty(&serde_json::json!({"id": p.id, "status": p.status})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Add a member to a project.")]
fn add_member(&self, Parameters(i): Parameters<AddMemberInput>) -> String {
match self.store.add_member(&i.id, &i.member) {
Ok(p) => serde_json::to_string_pretty(&serde_json::json!({"id": p.id, "members": p.members})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Create a work item (epic/story/task/bug/subtask). Returns a task id keyed by the project (e.g. APOLLO-12).")]
fn create_task(&self, Parameters(i): Parameters<CreateTaskInput>) -> String {
match self.store.create_task(&i.project_id, i.task_type, i.title, i.description, i.priority, i.reporter, i.assignee, i.parent_id, i.estimate, date(&i.due_date)) {
Ok(t) => serde_json::to_string_pretty(&serde_json::json!({"task_id": t.id, "type": t.task_type, "status": t.status, "priority": t.priority})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Get a task's full detail including dependencies, comments, and time logs.")]
fn get_task(&self, Parameters(i): Parameters<IdInput>) -> String {
match self.store.get_task(&i.id) {
Some(t) => serde_json::to_string_pretty(&t).unwrap(),
None => format!("Task not found: {}", i.id),
}
}
#[tool(description = "Search tasks by project, free text, status, assignee, type, sprint, or label.")]
fn search_tasks(&self, Parameters(i): Parameters<SearchTasksInput>) -> String {
let ts = self.store.search_tasks(i.project_id.as_deref(), i.query.as_deref(), i.status, i.assignee.as_deref(), i.task_type, i.sprint_id.as_deref(), i.label.as_deref());
let out: Vec<serde_json::Value> = ts.iter().map(|t| serde_json::json!({"id": t.id, "title": t.title, "type": t.task_type, "status": t.status, "priority": t.priority, "assignee": t.assignee})).collect();
serde_json::to_string_pretty(&serde_json::json!({"count": out.len(), "tasks": out})).unwrap()
}
#[tool(description = "Update task fields (title, description, priority, assignee, estimate, due_date).")]
fn update_task(&self, Parameters(i): Parameters<UpdateTaskInput>) -> String {
let assignee = i.assignee.clone().map(Some);
match self.store.update_task(&i.id, i.title, i.description, i.priority, assignee, i.estimate, date(&i.due_date)) {
Ok(t) => serde_json::to_string_pretty(&serde_json::json!({"id": t.id, "updated": true})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Transition a task through its workflow (backlog→todo→in_progress→in_review→done, plus blocked/cancelled). Enforces valid transitions and blocks done/in-progress while unfinished blockers remain. Gated.")]
fn transition_task(&self, Parameters(i): Parameters<TransitionTaskInput>) -> String {
match self.store.transition_task(&i.id, i.status) {
Ok(t) => serde_json::to_string_pretty(&serde_json::json!({"id": t.id, "status": t.status, "completed_at": t.completed_at})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Assign or unassign a task (pass null assignee to unassign).")]
fn assign_task(&self, Parameters(i): Parameters<AssignTaskInput>) -> String {
match self.store.assign_task(&i.id, i.assignee) {
Ok(t) => serde_json::to_string_pretty(&serde_json::json!({"id": t.id, "assignee": t.assignee})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Set a task's labels (replaces the existing set).")]
fn set_labels(&self, Parameters(i): Parameters<SetLabelsInput>) -> String {
match self.store.set_labels(&i.id, i.labels) {
Ok(t) => serde_json::to_string_pretty(&serde_json::json!({"id": t.id, "labels": t.labels})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Add a dependency between tasks (blocked_by | blocks | relates_to). Rejects self-links and cycles.")]
fn add_dependency(&self, Parameters(i): Parameters<DependencyInput>) -> String {
match self.store.add_dependency(&i.task_id, i.dep_type, &i.other_id) {
Ok(t) => serde_json::to_string_pretty(&serde_json::json!({"id": t.id, "dependencies": t.dependencies})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Add a comment to a task.")]
fn add_comment(&self, Parameters(i): Parameters<CommentInput>) -> String {
match self.store.add_comment(&i.task_id, &i.author, &i.body) {
Ok(c) => serde_json::to_string_pretty(&serde_json::json!({"comment_id": c.id, "added": true})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Log time (hours) against a task.")]
fn log_time(&self, Parameters(i): Parameters<LogTimeInput>) -> String {
match self.store.log_time(&i.task_id, &i.user, i.hours, i.note) {
Ok(l) => serde_json::to_string_pretty(&serde_json::json!({"time_log_id": l.id, "hours": l.hours, "logged": true})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Create a sprint/iteration in a project.")]
fn create_sprint(&self, Parameters(i): Parameters<CreateSprintInput>) -> String {
match self.store.create_sprint(&i.project_id, i.name, i.goal, date(&i.start_date), date(&i.end_date)) {
Ok(s) => serde_json::to_string_pretty(&serde_json::json!({"sprint_id": s.id, "name": s.name, "status": s.status})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Change a sprint's status (planned/active/closed). Gated.")]
fn set_sprint_status(&self, Parameters(i): Parameters<SetSprintStatusInput>) -> String {
match self.store.set_sprint_status(&i.id, i.status) {
Ok(s) => serde_json::to_string_pretty(&serde_json::json!({"id": s.id, "status": s.status})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Add or remove a task from a sprint (pass null sprint_id to remove from sprint / move to backlog).")]
fn assign_to_sprint(&self, Parameters(i): Parameters<AssignSprintInput>) -> String {
match self.store.assign_to_sprint(&i.task_id, i.sprint_id) {
Ok(t) => serde_json::to_string_pretty(&serde_json::json!({"id": t.id, "sprint_id": t.sprint_id})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "List sprints in a project.")]
fn list_sprints(&self, Parameters(i): Parameters<ProjectScopeInput>) -> String {
let v = self.store.list_sprints(&i.project_id);
serde_json::to_string_pretty(&serde_json::json!({"count": v.len(), "sprints": v})).unwrap()
}
#[tool(description = "Create a milestone in a project.")]
fn create_milestone(&self, Parameters(i): Parameters<CreateMilestoneInput>) -> String {
match self.store.create_milestone(&i.project_id, i.name, i.description, date(&i.due_date)) {
Ok(m) => serde_json::to_string_pretty(&serde_json::json!({"milestone_id": m.id, "name": m.name, "status": m.status})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Change a milestone's status (open/reached/missed). Gated.")]
fn set_milestone_status(&self, Parameters(i): Parameters<SetMilestoneStatusInput>) -> String {
match self.store.set_milestone_status(&i.id, i.status) {
Ok(m) => serde_json::to_string_pretty(&serde_json::json!({"id": m.id, "status": m.status})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Attach or detach a task from a milestone (pass null milestone_id to detach).")]
fn assign_to_milestone(&self, Parameters(i): Parameters<AssignMilestoneInput>) -> String {
match self.store.assign_to_milestone(&i.task_id, i.milestone_id) {
Ok(t) => serde_json::to_string_pretty(&serde_json::json!({"id": t.id, "milestone_id": t.milestone_id})).unwrap(),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "List milestones in a project.")]
fn list_milestones(&self, Parameters(i): Parameters<ProjectScopeInput>) -> String {
let v = self.store.list_milestones(&i.project_id);
serde_json::to_string_pretty(&serde_json::json!({"count": v.len(), "milestones": v})).unwrap()
}
#[tool(description = "Project progress report: task counts by status, completion %, story points done/total, and hours logged.")]
fn project_progress(&self, Parameters(i): Parameters<ProjectScopeInput>) -> String {
match self.store.project_progress(&i.project_id) {
Some(v) => serde_json::to_string_pretty(&v).unwrap(),
None => format!("Project not found: {}", i.project_id),
}
}
#[tool(description = "Sprint report: scope, completed count, and story points completed/remaining (burndown inputs).")]
fn sprint_report(&self, Parameters(i): Parameters<IdInput>) -> String {
match self.store.sprint_report(&i.id) {
Some(v) => serde_json::to_string_pretty(&v).unwrap(),
None => format!("Sprint not found: {}", i.id),
}
}
#[tool(description = "Critical path: the longest chain of blocked_by dependencies in the project (a schedule-risk proxy).")]
fn critical_path(&self, Parameters(i): Parameters<ProjectScopeInput>) -> String {
serde_json::to_string_pretty(&self.store.critical_path(&i.project_id)).unwrap()
}
#[tool(description = "Workload report: open/done task counts and story points per assignee for a project.")]
fn workload(&self, Parameters(i): Parameters<ProjectScopeInput>) -> String {
serde_json::to_string_pretty(&self.store.workload(&i.project_id)).unwrap()
}
}
#[async_trait::async_trait]
impl HealthCheck for ProjectServer {
async fn check_health(&self) -> HealthStatus {
HealthStatus { healthy: true, message: Some("operational".into()), latency_ms: Some(1) }
}
}