//! Public MCP ticket tools. //! //! The public surface says `ticket`, while the provider behind this trait maps //! to the application/domain `Issue` use cases. use async_trait::async_trait; use domain::Project; use serde_json::{json, Value}; use super::tools::ToolDef; /// Error returned by an MCP ticket tool provider. #[derive(Debug, Clone, thiserror::Error)] #[error("{code}: {message}")] pub struct TicketToolError { /// Stable machine-readable code. pub code: &'static str, /// Human-readable message. pub message: String, } impl TicketToolError { /// Builds a typed ticket-tool error. #[must_use] pub fn new(code: &'static str, message: impl Into) -> Self { Self { code, message: message.into(), } } /// Serialises the error as a camelCase JSON object. #[must_use] pub fn to_value(&self) -> Value { json!({ "code": self.code, "message": self.message }) } } /// Provider injected by the composition root to execute public ticket tools. #[async_trait] pub trait TicketToolProvider: Send + Sync { /// Executes one public ticket/sprint planning tool for `project`. async fn handle_ticket_tool( &self, project: &Project, requester: &str, name: &str, arguments: Value, ) -> Result; } /// Returns true when `name` is one of the public ticket tools. #[must_use] pub fn is_ticket_tool(name: &str) -> bool { matches!( name, "idea_ticket_create" | "idea_ticket_read" | "idea_ticket_list" | "idea_ticket_update" | "idea_ticket_update_status" | "idea_ticket_update_priority" | "idea_ticket_read_carnet" | "idea_ticket_update_carnet" | "idea_ticket_link" | "idea_ticket_unlink" | "idea_sprint_list" ) } /// Public ticket tool definitions advertised by `tools/list`. #[must_use] pub fn catalogue() -> Vec { let ticket_ref = json!({ "type": "string", "pattern": "^#[1-9][0-9]*$" }); let status = json!({ "type": "string", "enum": ["open", "inProgress", "QA", "closed"] }); let priority = json!({ "type": "string", "enum": ["low", "medium", "high", "critical"] }); let sort = json!({ "type": "object", "properties": { "field": { "type": "string", "enum": ["number", "priority", "status", "title"] }, "direction": { "type": "string", "enum": ["asc", "desc"] } }, "required": ["field", "direction"], "additionalProperties": false }); let link_kind = json!({ "type": "string", "enum": ["relatesTo", "blocks", "blockedBy", "duplicates", "dependsOn"] }); let link = json!({ "type": "object", "properties": { "targetRef": ticket_ref.clone(), "kind": link_kind.clone() }, "required": ["targetRef", "kind"], "additionalProperties": false }); vec![ ToolDef { name: "idea_ticket_create", description: "Create an IdeA ticket in the current project and return it as JSON.", input_schema: json!({ "type": "object", "properties": { "title": { "type": "string" }, "description": { "type": "string" }, "priority": priority.clone(), "status": status.clone(), "assignedAgentIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "links": { "type": "array", "items": link.clone() } }, "required": ["title"], "additionalProperties": false }), }, ToolDef { name: "idea_ticket_read", description: "Read one IdeA ticket by public reference (#N).", input_schema: json!({ "type": "object", "properties": { "ref": ticket_ref.clone(), "includeCarnet": { "type": "boolean" } }, "required": ["ref"], "additionalProperties": false }), }, ToolDef { name: "idea_ticket_list", description: "List IdeA tickets in the current project.", input_schema: json!({ "type": "object", "properties": { "statuses": { "type": "array", "items": status.clone(), "uniqueItems": true }, "priorities": { "type": "array", "items": priority.clone(), "uniqueItems": true }, "assignedAgentId": { "type": "string", "format": "uuid" }, "text": { "type": "string" }, "sort": sort.clone(), "limit": { "type": "integer", "minimum": 1 }, "cursor": { "type": "string" } }, "additionalProperties": false }), }, ToolDef { name: "idea_ticket_update", description: "Update ticket fields using optimistic concurrency.", input_schema: json!({ "type": "object", "properties": { "ref": ticket_ref.clone(), "title": { "type": "string" }, "description": { "type": "string" }, "status": status.clone(), "priority": priority.clone(), "assignedAgentIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "expectedVersion": { "type": "integer", "minimum": 1 } }, "required": ["ref", "expectedVersion"], "additionalProperties": false }), }, ToolDef { name: "idea_ticket_update_status", description: "Update a ticket status using optimistic concurrency.", input_schema: json!({ "type": "object", "properties": { "ref": ticket_ref.clone(), "status": status.clone(), "expectedVersion": { "type": "integer", "minimum": 1 } }, "required": ["ref", "status", "expectedVersion"], "additionalProperties": false }), }, ToolDef { name: "idea_ticket_update_priority", description: "Update a ticket priority using optimistic concurrency.", input_schema: json!({ "type": "object", "properties": { "ref": ticket_ref.clone(), "priority": priority.clone(), "expectedVersion": { "type": "integer", "minimum": 1 } }, "required": ["ref", "priority", "expectedVersion"], "additionalProperties": false }), }, ToolDef { name: "idea_ticket_read_carnet", description: "Read the editable carnet attached to a ticket.", input_schema: json!({ "type": "object", "properties": { "ref": ticket_ref.clone() }, "required": ["ref"], "additionalProperties": false }), }, ToolDef { name: "idea_ticket_update_carnet", description: "Update a ticket carnet using optimistic concurrency.", input_schema: json!({ "type": "object", "properties": { "ref": ticket_ref.clone(), "carnet": { "type": "string" }, "expectedVersion": { "type": "integer", "minimum": 1 } }, "required": ["ref", "carnet", "expectedVersion"], "additionalProperties": false }), }, ToolDef { name: "idea_ticket_link", description: "Add a link from one ticket to another using optimistic concurrency.", input_schema: json!({ "type": "object", "properties": { "ref": ticket_ref.clone(), "targetRef": ticket_ref.clone(), "kind": link_kind.clone(), "expectedVersion": { "type": "integer", "minimum": 1 } }, "required": ["ref", "targetRef", "kind", "expectedVersion"], "additionalProperties": false }), }, ToolDef { name: "idea_ticket_unlink", description: "Remove a link from one ticket to another using optimistic concurrency.", input_schema: json!({ "type": "object", "properties": { "ref": ticket_ref.clone(), "targetRef": ticket_ref, "kind": link_kind, "expectedVersion": { "type": "integer", "minimum": 1 } }, "required": ["ref", "targetRef", "expectedVersion"], "additionalProperties": false }), }, ToolDef { name: "idea_sprint_list", description: "List IdeA sprints in execution order. Read-only.", input_schema: json!({ "type": "object", "properties": {}, "additionalProperties": false }), }, ] }