arcade-mcp/toolkits/jira/arcade_jira/tools/transitions.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

145 lines
5 KiB
Python

from typing import Annotated, cast
from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Atlassian
from arcade_jira.client import JiraClient
@tool(requires_auth=Atlassian(scopes=["read:jira-work"]))
async def get_transition_by_id(
context: ToolContext,
issue: Annotated[str, "The ID or key of the issue"],
transition_id: Annotated[str, "The ID of the transition"],
) -> Annotated[dict, "The transition data"]:
"""Get a transition by its ID."""
if not transition_id:
return {"error": "The transition ID is required."}
if not transition_id.isdigit():
return {"error": "The transition ID must be a numeric string."}
client = JiraClient(context.get_auth_token_or_empty())
response = await client.get(
f"/issue/{issue}/transitions",
params={
"transitionId": transition_id,
},
)
transitions = response["transitions"]
if len(transitions) == 0:
return {
"error": (
f"No transition found for the issue '{issue}' with ID '{transition_id}'. "
"To get all transitions available for the issue, use the "
f"`Jira.{get_transitions_available_for_issue.__tool_name__}` tool."
),
}
if len(transitions) == 1:
return {"transition": transitions[0]}
return {
"error": f"Multiple transitions found for the issue '{issue}' with ID '{transition_id}'.",
"transitions": transitions,
}
@tool(requires_auth=Atlassian(scopes=["read:jira-work"]))
async def get_transitions_available_for_issue(
context: ToolContext,
issue: Annotated[str, "The ID or key of the issue"],
) -> Annotated[dict, "The transitions available and the issue's current status"]:
"""Get the transitions available for an existing Jira issue."""
from arcade_jira.tools.issues import get_issue_by_id # Avoid circular import
client = JiraClient(context.get_auth_token_or_empty())
issue_data = await get_issue_by_id(context, issue)
if issue_data.get("error"):
return cast(dict, issue_data)
response = await client.get(
f"/issue/{issue_data['issue']['id']}/transitions",
params={
"expand": "transitions.fields",
},
)
return {
"issue": {
"id": issue_data["issue"]["id"],
"key": issue_data["issue"]["key"],
"current_status": issue_data["issue"]["status"],
},
"transitions_available": response["transitions"],
}
@tool(
requires_auth=Atlassian(
scopes=[
"read:jira-work", # Needed to get the transitions available for the issue
"write:jira-work", # Needed to transition the issue
],
),
)
async def get_transition_by_status_name(
context: ToolContext,
issue: Annotated[str, "The ID or key of the issue"],
transition: Annotated[str, "The name of the transition status"],
) -> Annotated[dict, "The transition data, including screen fields available"]:
"""Get a transition available for an issue by the transition name.
The response will contain screen fields available for the transition, if any.
"""
transitions = await get_transitions_available_for_issue(context, issue)
for available_transition in transitions["transitions_available"]:
if available_transition["name"].casefold() == transition.casefold():
return {"issue": issue, "transition": available_transition}
return {
"error": f"Transition '{transition}' not found for the issue '{issue}'",
"transitions_available": transitions["transitions_available"],
}
@tool(
requires_auth=Atlassian(
scopes=[
"read:jira-work", # Needed to get the transition ID by name
"write:jira-work", # Needed to transition the issue
],
),
)
async def transition_issue_to_new_status(
context: ToolContext,
issue: Annotated[str, "The ID or key of the issue"],
transition: Annotated[
str,
"The transition to perform. Provide the transition ID or its name (case insensitive).",
],
) -> Annotated[dict, "The updated issue"]:
"""Transition a Jira issue to a new status."""
client = JiraClient(context.get_auth_token_or_empty())
# Try to get the transition by ID first
response = await get_transition_by_id(context, issue, transition)
# If the transition is not found by ID, try to get it by name
if response.get("error"):
response = await get_transition_by_status_name(context, issue, transition)
if response.get("error"):
return cast(dict, response)
transition_id = response["transition"]["id"]
transition_name = response["transition"]["name"]
# The /issue/issue_id/transitions endpoint returns a 204 No Content in case of success
await client.post(
f"/issue/{issue}/transitions",
json_data={
"transition": {"id": transition_id},
},
)
return {
"status": "success",
"message": f"Issue '{issue}' successfully transitioned to '{transition_name}'.",
}