arcade-mcp/toolkits/jira/arcade_jira/tools/comments.py
Renato Byrro a538f4bf81
Jira Toolkit (#407)
Unusually big PR, in large part due to Jira's data model complexities.
2025-06-11 12:06:33 -03:00

164 lines
6 KiB
Python

from typing import Annotated, Any
from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Atlassian
from arcade.sdk.errors import ToolExecutionError
from arcade_jira.client import JiraClient
from arcade_jira.constants import IssueCommentOrderBy
from arcade_jira.exceptions import MultipleItemsFoundError, NotFoundError
from arcade_jira.utils import (
add_pagination_to_response,
build_adf_doc,
clean_comment_dict,
find_multiple_unique_users,
remove_none_values,
)
@tool(requires_auth=Atlassian(scopes=["read:jira-work"]))
async def get_comment_by_id(
context: ToolContext,
issue_id: Annotated[str, "The ID or key of the issue to retrieve the comment from."],
comment_id: Annotated[str, "The ID of the comment to retrieve"],
include_adf_content: Annotated[
bool,
"Whether to include the ADF (Atlassian Document Format) content of the comment in the "
"response. Defaults to False (return only the HTML rendered content).",
] = False,
) -> Annotated[dict[str, Any], "Information about the comment"]:
"""Get a comment by its ID."""
client = JiraClient(context.get_auth_token_or_empty())
response = await client.get(
f"issue/{issue_id}/comment/{comment_id}",
params={"expand": "renderedBody"},
)
if not response:
return {
"comment": None,
"message": f"No comment found with ID '{comment_id}' in the issue '{issue_id}'.",
"query": {"issue_id": issue_id, "comment_id": comment_id},
}
return {"comment": clean_comment_dict(response, include_adf_content)}
@tool(requires_auth=Atlassian(scopes=["read:jira-work"]))
async def get_issue_comments(
context: ToolContext,
issue: Annotated[str, "The ID or key of the issue to retrieve"],
limit: Annotated[
int,
"The maximum number of comments to retrieve. Min 1, max 100, default 100.",
] = 100,
offset: Annotated[
int,
"The number of comments to skip. Defaults to 0 (start from the first comment).",
] = 0,
order_by: Annotated[
IssueCommentOrderBy | None,
"The order in which to return the comments. "
f"Defaults to '{IssueCommentOrderBy.CREATED_DATE_DESCENDING.value}' (most recent first).",
] = IssueCommentOrderBy.CREATED_DATE_DESCENDING,
include_adf_content: Annotated[
bool,
"Whether to include the ADF (Atlassian Document Format) content of the comment in the "
"response. Defaults to False (return only the HTML rendered content).",
] = False,
) -> Annotated[dict[str, Any], "Information about the issue comments"]:
"""Get the comments of a Jira issue by its ID."""
limit = max(min(limit, 100), 1)
client = JiraClient(context.get_auth_token_or_empty())
api_response = await client.get(
f"issue/{issue}/comment",
params=remove_none_values({
"expand": "renderedBody",
"maxResults": limit,
"startAt": offset,
"orderBy": order_by.to_api_value() if order_by else None,
}),
)
comments = [
clean_comment_dict(comment, include_adf_content)
for comment in api_response["comments"][:limit]
]
response = {
"issue": issue,
"comments": comments,
"isLast": api_response.get("isLast"),
}
return add_pagination_to_response(response, comments, limit, offset)
@tool(
requires_auth=Atlassian(
scopes=[
"write:jira-work", # Needed to add the comment
"read:jira-work", # Needed to get the issue data
"read:jira-user", # Needed to resolve user ID from name or email (mention_users)
],
),
)
async def add_comment_to_issue(
context: ToolContext,
issue: Annotated[str, "The ID or key of the issue to comment on."],
body: Annotated[str, "The body of the comment to add to the issue."],
reply_to_comment: Annotated[
str | None,
"Quote a previous comment as a reply to it. Provide the comment's ID. "
"Must be a comment from the same issue. Defaults to None (no quoted comment).",
] = None,
mention_users: Annotated[
list[str] | None,
"The users to mention in the comment. Provide the user display name, email address, or ID. "
"Ex: 'John Doe' or 'john.doe@example.com'. Defaults to None (no user mentions).",
] = None,
) -> Annotated[dict[str, Any], "Information about the comment created"]:
"""Add a comment to a Jira issue."""
if not body:
raise ToolExecutionError("Comment body cannot be empty.")
client = JiraClient(context.get_auth_token_or_empty())
adf_body = build_adf_doc(body)
if mention_users:
try:
users = await find_multiple_unique_users(context, mention_users, exact_match=True)
except (NotFoundError, MultipleItemsFoundError) as exc:
return {"error": f"Failed to mention user: {exc.message}"}
mentions = [
{
"type": "mention",
"attrs": {"accessLevel": "", "id": user["id"], "text": f"@{user['name']}"},
}
for user in users
]
adf_body["content"][0]["content"] = mentions + adf_body["content"][0]["content"]
if reply_to_comment:
quote_comment = await get_comment_by_id(context, issue, reply_to_comment, True)
if not quote_comment["comment"]:
raise ToolExecutionError(
f"Cannot quote comment. No comment found with ID '{reply_to_comment}'."
)
quote = {
"type": "blockquote",
"content": quote_comment["comment"]["adf_body"]["content"],
}
adf_body["content"] = [quote] + adf_body["content"]
response = await client.post(
f"issue/{issue}/comment",
json_data={
"expand": "renderedBody",
"body": adf_body,
},
)
return {
"success": True,
"message": f"Comment successfully created for the issue '{issue}'.",
"comment": {"id": response["id"], "created_at": response["created"]},
}