Skip to content
Clone
eba4fd1
1123 lines 25.9 KB Rust
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<Uuid, CallToolResult> {
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::<IssueState>(raw) {
Ok(state) => Some(state),
Err(error) => return error,
},
None => None,
};
let priority = match args.priority.as_deref() {
Some(raw) => match parse_enum::<IssuePriority>(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::<IssuePriority>(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::<IssuePriority>(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::<IssueMemberKind>(&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),
}
}