use issues::services::boards::{CreateBoardCmd, ListBoardQuery, UpdateBoardCmd}; use issues::services::comments::{CreateCommentCmd, ListCommentQuery, UpdateCommentCmd}; use issues::services::dependencies::CreateDependencyCmd; use issues::services::issues::{CreateIssueCmd, IssueState, ListIssueQuery, UpdateIssueCmd}; use issues::services::labels::{CreateLabelCmd, ListLabelQuery, UpdateLabelCmd}; use issues::services::members::{CreateIssueMemberCmd, ListIssueMemberQuery}; use issues::services::milestones::{CreateMilestoneCmd, ListMilestoneQuery, UpdateMilestoneCmd}; use issues::services::pins::{CreatePinCmd, UpdatePinCmd}; use models::issue::issue_item::IssuePriority; use models::issue::issue_member::IssueMemberKind; use rmcp::model::CallToolResult; use uuid::Uuid; use crate::auth::Principal; use crate::error::{tool_done, tool_error, tool_ok, write_denied}; use crate::services::McpServices; use crate::tools::args; use crate::tools::helpers::{parse_enum, parse_time}; async fn resolve_issue( services: &McpServices, principal: &Principal, space: &str, board_id: Uuid, reference: &str, ) -> Result { services .issues .resolve_issue_ref(principal.uid, space.trim(), board_id, reference.trim()) .await .map_err(tool_error) } pub(crate) async fn board_list( services: &McpServices, principal: &Principal, args: args::IssueBoardListArgs, ) -> CallToolResult { let query = ListBoardQuery { dir_id: args.dir_id, q: args.q, limit: args.limit, offset: args.offset, }; match services .issues .list_boards(principal.uid, args.space.trim(), query) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn board_get( services: &McpServices, principal: &Principal, args: args::IssueBoardRefArgs, ) -> CallToolResult { match services .issues .get_board(principal.uid, args.space.trim(), args.board_id) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn board_create( services: &McpServices, principal: &Principal, args: args::IssueBoardCreateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let cmd = CreateBoardCmd { name: args.name, parent_dir_id: args.parent_dir_id, icon: args.icon, description: args.description, }; match services .issues .create_board(principal.uid, args.space.trim(), cmd) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn board_update( services: &McpServices, principal: &Principal, args: args::IssueBoardUpdateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let cmd = UpdateBoardCmd { name: args.name, icon: args.icon.map(Some), description: args.description.map(Some), }; match services .issues .update_board(principal.uid, args.space.trim(), args.board_id, cmd) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn board_delete( services: &McpServices, principal: &Principal, args: args::IssueBoardRefArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } match services .issues .delete_board(principal.uid, args.space.trim(), args.board_id) .await { Ok(()) => tool_done(), Err(error) => tool_error(error), } } pub(crate) async fn list( services: &McpServices, principal: &Principal, args: args::IssueListArgs, ) -> CallToolResult { let state = match args.state.as_deref() { Some(raw) => match parse_enum::(raw) { Ok(state) => Some(state), Err(error) => return error, }, None => None, }; let priority = match args.priority.as_deref() { Some(raw) => match parse_enum::(raw) { Ok(priority) => Some(priority), Err(error) => return error, }, None => None, }; let query = ListIssueQuery { state, author_id: args.author_id, milestone_id: args.milestone_id, label_id: args.label_id, assignee_id: args.assignee_id, priority, sort: args.sort, q: args.q, limit: args.limit, offset: args.offset, }; match services .issues .list_issues(principal.uid, args.space.trim(), args.board_id, query) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn get( services: &McpServices, principal: &Principal, args: args::IssueRefArgs, ) -> CallToolResult { let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; match services .issues .get_issue(principal.uid, args.space.trim(), args.board_id, issue_id) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn create( services: &McpServices, principal: &Principal, args: args::IssueCreateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let priority = match args.priority.as_deref() { Some(raw) => match parse_enum::(raw) { Ok(priority) => priority, Err(error) => return error, }, None => IssuePriority::None, }; let due_at = match args.due_at.as_deref() { Some(raw) => match parse_time(raw) { Ok(time) => Some(time), Err(error) => return error, }, None => None, }; let cmd = CreateIssueCmd { title: args.title, description: args.description, content: args.content.unwrap_or_default(), milestone_id: args.milestone_id, priority, due_at, assignee_ids: args.assignee_ids, label_ids: args.label_ids, }; match services .issues .create_issue(principal.uid, args.space.trim(), args.board_id, cmd) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn update( services: &McpServices, principal: &Principal, args: args::IssueUpdateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; let priority = match args.priority.as_deref() { Some(raw) => match parse_enum::(raw) { Ok(priority) => Some(priority), Err(error) => return error, }, None => None, }; let due_at = match args.due_at.as_deref() { Some(raw) => match parse_time(raw) { Ok(time) => Some(Some(time)), Err(error) => return error, }, None => None, }; let cmd = UpdateIssueCmd { title: args.title, description: args.description.map(Some), content: args.content, milestone_id: args.milestone_id.map(Some), priority, due_at, assignee_ids: args.assignee_ids, label_ids: args.label_ids, }; match services .issues .update_issue( principal.uid, args.space.trim(), args.board_id, issue_id, cmd, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn delete( services: &McpServices, principal: &Principal, args: args::IssueRefArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; match services .issues .delete_issue(principal.uid, args.space.trim(), args.board_id, issue_id) .await { Ok(()) => tool_done(), Err(error) => tool_error(error), } } pub(crate) async fn set_closed( services: &McpServices, principal: &Principal, args: args::IssueRefArgs, closed: bool, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; match services .issues .close_issue( principal.uid, args.space.trim(), args.board_id, issue_id, closed, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn comment_list( services: &McpServices, principal: &Principal, args: args::IssueCommentListArgs, ) -> CallToolResult { let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; let query = ListCommentQuery { parent_id: args.parent_id, limit: args.limit, offset: args.offset, sort: args.sort, }; match services .issues .list_comments( principal.uid, args.space.trim(), args.board_id, issue_id, query, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn comment_create( services: &McpServices, principal: &Principal, args: args::IssueCommentCreateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; let cmd = CreateCommentCmd { content: args.content, parent_id: args.parent_id, }; match services .issues .create_comment( principal.uid, args.space.trim(), args.board_id, issue_id, cmd, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn comment_update( services: &McpServices, principal: &Principal, args: args::IssueCommentUpdateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; let cmd = UpdateCommentCmd { content: args.content, }; match services .issues .update_comment( principal.uid, args.space.trim(), args.board_id, issue_id, args.comment_id, cmd, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn comment_delete( services: &McpServices, principal: &Principal, args: args::IssueCommentRefArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; match services .issues .delete_comment( principal.uid, args.space.trim(), args.board_id, issue_id, args.comment_id, ) .await { Ok(()) => tool_done(), Err(error) => tool_error(error), } } pub(crate) async fn label_list( services: &McpServices, principal: &Principal, args: args::IssueLabelListArgs, ) -> CallToolResult { let query = ListLabelQuery { q: args.q, is_archived: Some(false), limit: args.limit, offset: args.offset, }; match services .issues .list_labels(principal.uid, args.space.trim(), args.board_id, query) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn label_create( services: &McpServices, principal: &Principal, args: args::IssueLabelCreateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let cmd = CreateLabelCmd { name: args.name, color: args.color, exclusive: args.exclusive, exclusive_order: None, description: args.description, }; match services .issues .create_label(principal.uid, args.space.trim(), args.board_id, cmd) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn label_update( services: &McpServices, principal: &Principal, args: args::IssueLabelUpdateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let cmd = UpdateLabelCmd { name: args.name, color: args.color, exclusive: args.exclusive, exclusive_order: None, description: args.description.map(Some), is_archived: args.is_archived, }; match services .issues .update_label( principal.uid, args.space.trim(), args.board_id, args.label_id, cmd, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn label_delete( services: &McpServices, principal: &Principal, args: args::IssueLabelRefArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } match services .issues .delete_label( principal.uid, args.space.trim(), args.board_id, args.label_id, ) .await { Ok(()) => tool_done(), Err(error) => tool_error(error), } } pub(crate) async fn label_bind( services: &McpServices, principal: &Principal, args: args::IssueLabelBindArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; match services .issues .bind_label( principal.uid, args.space.trim(), args.board_id, issue_id, args.label_id, ) .await { Ok(()) => tool_done(), Err(error) => tool_error(error), } } pub(crate) async fn label_unbind( services: &McpServices, principal: &Principal, args: args::IssueLabelBindArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; match services .issues .unbind_label( principal.uid, args.space.trim(), args.board_id, issue_id, args.label_id, ) .await { Ok(()) => tool_done(), Err(error) => tool_error(error), } } pub(crate) async fn milestone_list( services: &McpServices, principal: &Principal, args: args::IssueMilestoneListArgs, ) -> CallToolResult { let query = ListMilestoneQuery { is_closed: args.is_closed, q: args.q, limit: args.limit, offset: args.offset, }; match services .issues .list_milestones(principal.uid, args.space.trim(), args.board_id, query) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn milestone_create( services: &McpServices, principal: &Principal, args: args::IssueMilestoneCreateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let due_at = match args.due_at.as_deref() { Some(raw) => match parse_time(raw) { Ok(time) => Some(time), Err(error) => return error, }, None => None, }; let cmd = CreateMilestoneCmd { name: args.name, description: args.description, due_at, }; match services .issues .create_milestone(principal.uid, args.space.trim(), args.board_id, cmd) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn milestone_update( services: &McpServices, principal: &Principal, args: args::IssueMilestoneUpdateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let due_at = match args.due_at.as_deref() { Some(raw) => match parse_time(raw) { Ok(time) => Some(Some(time)), Err(error) => return error, }, None => None, }; let cmd = UpdateMilestoneCmd { name: args.name, description: args.description.map(Some), due_at, is_closed: args.is_closed, }; match services .issues .update_milestone( principal.uid, args.space.trim(), args.board_id, args.milestone_id, cmd, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn milestone_delete( services: &McpServices, principal: &Principal, args: args::IssueMilestoneRefArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } match services .issues .delete_milestone( principal.uid, args.space.trim(), args.board_id, args.milestone_id, ) .await { Ok(()) => tool_done(), Err(error) => tool_error(error), } } pub(crate) async fn member_list( services: &McpServices, principal: &Principal, args: args::IssueMemberListArgs, ) -> CallToolResult { let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; let query = ListIssueMemberQuery { kind: None, limit: args.limit, offset: args.offset, }; match services .issues .list_members( principal.uid, args.space.trim(), args.board_id, issue_id, query, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn member_create( services: &McpServices, principal: &Principal, args: args::IssueMemberCreateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; let kind = match parse_enum::(&args.kind) { Ok(kind) => kind, Err(error) => return error, }; let cmd = CreateIssueMemberCmd { user_id: args.user_id, kind, }; match services .issues .create_member( principal.uid, args.space.trim(), args.board_id, issue_id, cmd, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn member_delete( services: &McpServices, principal: &Principal, args: args::IssueMemberRefArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; match services .issues .delete_member( principal.uid, args.space.trim(), args.board_id, issue_id, args.member_id, ) .await { Ok(()) => tool_done(), Err(error) => tool_error(error), } } pub(crate) async fn pin_list( services: &McpServices, principal: &Principal, args: args::IssuePinListArgs, ) -> CallToolResult { match services .issues .list_pins(principal.uid, args.space.trim(), args.board_id) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn pin_create( services: &McpServices, principal: &Principal, args: args::IssuePinCreateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; let cmd = CreatePinCmd { issue_id, pin_order: args.pin_order.unwrap_or(0), }; match services .issues .create_pin(principal.uid, args.space.trim(), args.board_id, cmd) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn pin_update( services: &McpServices, principal: &Principal, args: args::IssuePinUpdateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let cmd = UpdatePinCmd { pin_order: args.pin_order, }; match services .issues .update_pin( principal.uid, args.space.trim(), args.board_id, args.pin_id, cmd, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn pin_delete( services: &McpServices, principal: &Principal, args: args::IssuePinRefArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } match services .issues .delete_pin(principal.uid, args.space.trim(), args.board_id, args.pin_id) .await { Ok(()) => tool_done(), Err(error) => tool_error(error), } } pub(crate) async fn dependency_list( services: &McpServices, principal: &Principal, args: args::IssueDependencyListArgs, ) -> CallToolResult { let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; match services .issues .list_dependencies(principal.uid, args.space.trim(), args.board_id, issue_id) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn dependency_create( services: &McpServices, principal: &Principal, args: args::IssueDependencyCreateArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; let cmd = CreateDependencyCmd { dependency_id: args.dependency_id, }; match services .issues .create_dependency( principal.uid, args.space.trim(), args.board_id, issue_id, cmd, ) .await { Ok(result) => tool_ok(&result), Err(error) => tool_error(error), } } pub(crate) async fn dependency_delete( services: &McpServices, principal: &Principal, args: args::IssueDependencyRefArgs, ) -> CallToolResult { if !principal.can_write() { return write_denied(); } let issue_id = match resolve_issue( services, principal, &args.space, args.board_id, &args.issue_id, ) .await { Ok(id) => id, Err(error) => return error, }; match services .issues .delete_dependency( principal.uid, args.space.trim(), args.board_id, issue_id, args.dep_id, ) .await { Ok(()) => tool_done(), Err(error) => tool_error(error), } }