Skip to content

Commit 86a5ab2

Browse files
committed
feat: enhance JIRA error handling and extend JSON field support to update operations
- Extend JSON string parsing support to jira_update_issue operations - Implement comprehensive error handling with detailed JIRA API response parsing - Add individual field update fallback when batch operations fail - Include field format analysis with suggestions for common JIRA field types - Replace exceptions with structured JSON error responses for better MCP client integration Builds upon the JSON additional_fields parsing foundation established by @4erdenko.
1 parent 44ef605 commit 86a5ab2

File tree

4 files changed

+1289
-874
lines changed

4 files changed

+1289
-874
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -726,8 +726,7 @@ Here's a complete example of setting up multi-user authentication with streamabl
726726
727727
- `jira_get_issue`: Get details of a specific issue
728728
- `jira_search`: Search issues using JQL
729-
- `jira_create_issue`: Create a new issue (supports `additional_fields` as a
730-
dictionary or JSON string)
729+
- `jira_create_issue`: Create a new issue
731730
- `jira_update_issue`: Update an existing issue
732731
- `jira_transition_issue`: Transition an issue to a new status
733732
- `jira_add_comment`: Add a comment to an issue

src/mcp_atlassian/servers/jira.py

Lines changed: 329 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -708,21 +708,129 @@ async def create_issue(
708708
else:
709709
raise ValueError("additional_fields must be a dictionary or JSON string.")
710710

711-
issue = jira.create_issue(
712-
project_key=project_key,
713-
summary=summary,
714-
issue_type=issue_type,
715-
description=description,
716-
assignee=assignee,
717-
components=components_list,
718-
**extra_fields,
719-
)
720-
result = issue.to_simplified_dict()
721-
return json.dumps(
722-
{"message": "Issue created successfully", "issue": result},
723-
indent=2,
724-
ensure_ascii=False,
725-
)
711+
try:
712+
issue = jira.create_issue(
713+
project_key=project_key,
714+
summary=summary,
715+
issue_type=issue_type,
716+
description=description,
717+
assignee=assignee,
718+
components=components_list,
719+
**extra_fields,
720+
)
721+
result = issue.to_simplified_dict()
722+
return json.dumps(
723+
{"message": "Issue created successfully", "issue": result},
724+
indent=2,
725+
ensure_ascii=False,
726+
)
727+
except Exception as e:
728+
# Enhanced error handling with detailed information
729+
error_msg = f"Failed to create JIRA issue: {str(e)}"
730+
731+
# Extract JIRA API error details if available
732+
if hasattr(e, "response") and hasattr(e.response, "text"):
733+
try:
734+
jira_error = json.loads(e.response.text)
735+
error_msg += (
736+
f"\n\nJIRA API Response:\n{json.dumps(jira_error, indent=2)}"
737+
)
738+
except Exception:
739+
error_msg += f"\n\nJIRA API Response (raw):\n{e.response.text}"
740+
741+
# Try creating with individual fields if batch creation failed
742+
if extra_fields:
743+
logger.warning(
744+
f"Batch create failed, attempting individual field fallback for project {project_key}"
745+
)
746+
747+
# First create with core fields only
748+
try:
749+
core_issue = jira.create_issue(
750+
project_key=project_key,
751+
summary=summary,
752+
issue_type=issue_type,
753+
description=description,
754+
assignee=assignee,
755+
components=components_list,
756+
)
757+
758+
# Then attempt to update with additional fields using our enhanced update function
759+
success_fields = []
760+
failed_fields = []
761+
762+
for field_name, field_value in extra_fields.items():
763+
try:
764+
jira.update_issue(core_issue.key, **{field_name: field_value})
765+
success_fields.append(field_name)
766+
except Exception as field_error:
767+
failed_fields.append(
768+
{
769+
"field": field_name,
770+
"value": field_value,
771+
"error": str(field_error),
772+
"suggestion": analyze_field_format(
773+
field_name, field_value
774+
),
775+
}
776+
)
777+
778+
# Return partial success with details
779+
result = core_issue.to_simplified_dict()
780+
response_data = {
781+
"message": "Issue created with partial field updates",
782+
"issue": result,
783+
"field_update_results": {
784+
"successful_fields": success_fields,
785+
"failed_fields": failed_fields,
786+
},
787+
}
788+
789+
if failed_fields:
790+
response_data["troubleshooting"] = (
791+
"Some additional fields failed to update. Check field names, types, and permissions."
792+
)
793+
794+
return json.dumps(response_data, indent=2, ensure_ascii=False)
795+
796+
except Exception as fallback_error:
797+
error_msg += f"\n\nFallback creation also failed: {str(fallback_error)}"
798+
799+
# Add field format analysis for debugging
800+
if extra_fields:
801+
error_msg += "\n\nField Analysis:"
802+
for field_name, field_value in extra_fields.items():
803+
suggestion = analyze_field_format(field_name, field_value)
804+
error_msg += f"\n- {field_name}: {suggestion}"
805+
806+
logger.error(f"JIRA create_issue failed: {error_msg}")
807+
808+
# Return structured error response instead of raising exception
809+
# This ensures detailed error information reaches the MCP client
810+
error_response = {
811+
"success": False,
812+
"error": "Issue creation failed",
813+
"details": error_msg,
814+
"troubleshooting_guide": "Check field names, required fields, and permissions in your JIRA project",
815+
}
816+
817+
# Parse JIRA API errors if available
818+
if hasattr(e, "response") and hasattr(e.response, "text"):
819+
try:
820+
jira_error_details = json.loads(e.response.text)
821+
error_response["jira_api_response"] = jira_error_details
822+
except Exception:
823+
error_response["jira_api_response_raw"] = e.response.text
824+
825+
# Add field analysis for debugging
826+
if extra_fields:
827+
error_response["field_analysis"] = {}
828+
for field_name, field_value in extra_fields.items():
829+
error_response["field_analysis"][field_name] = analyze_field_format(
830+
field_name, field_value
831+
)
832+
833+
return json.dumps(error_response, indent=2, ensure_ascii=False)
726834

727835

728836
@jira_mcp.tool(tags={"jira", "write"})
@@ -866,6 +974,100 @@ async def batch_get_changelogs(
866974
return json.dumps(results, indent=2, ensure_ascii=False)
867975

868976

977+
def try_individual_field_updates(
978+
jira: Any, issue_key: str, failed_fields: dict[str, Any]
979+
) -> dict[str, Any]:
980+
"""Attempt to update fields individually to identify specific failures."""
981+
results: dict[str, dict[str, Any]] = {}
982+
successful_updates = {}
983+
984+
for field_name, field_value in failed_fields.items():
985+
try:
986+
single_field_update = {field_name: field_value}
987+
jira.update_issue(issue_key=issue_key, **single_field_update)
988+
results[field_name] = {"status": "success"}
989+
successful_updates[field_name] = field_value
990+
except Exception as field_error:
991+
results[field_name] = {
992+
"status": "failed",
993+
"error": str(field_error),
994+
"error_type": type(field_error).__name__,
995+
"format_analysis": analyze_field_format(field_name, field_value),
996+
}
997+
998+
# Try to extract JIRA-specific error details for individual fields
999+
if hasattr(field_error, "response") and field_error.response is not None:
1000+
results[field_name].update(
1001+
{
1002+
"http_status": getattr(
1003+
field_error.response, "status_code", None
1004+
),
1005+
"jira_response": getattr(field_error.response, "text", None),
1006+
}
1007+
)
1008+
1009+
return {
1010+
"individual_results": results,
1011+
"successful_updates": successful_updates,
1012+
"failed_count": len([r for r in results.values() if r["status"] == "failed"]),
1013+
"success_count": len([r for r in results.values() if r["status"] == "success"]),
1014+
}
1015+
1016+
1017+
def analyze_field_format(field_id: str, field_value: Any) -> dict[str, Any]:
1018+
"""Analyze field value and suggest potential format fixes."""
1019+
analysis = {
1020+
"field_id": field_id,
1021+
"current_value": field_value,
1022+
"current_type": type(field_value).__name__,
1023+
"suggestions": [],
1024+
}
1025+
1026+
# Check for boolean-like string values that might need object format
1027+
if isinstance(field_value, str):
1028+
lower_val = field_value.lower()
1029+
if lower_val in ["yes", "no", "true", "false"]:
1030+
analysis["suggestions"].extend(
1031+
[
1032+
f"Try boolean object format: {{'value': '{field_value}'}}",
1033+
f"Try boolean direct format: {lower_val in ['true', 'yes']}",
1034+
]
1035+
)
1036+
1037+
# Check for select/option fields that might need object format
1038+
if "customfield" in field_id:
1039+
analysis["suggestions"].extend(
1040+
[
1041+
f"Try select object format: {{'value': '{field_value}'}}",
1042+
f"Try select with ID format: {{'id': '{field_value}'}}",
1043+
]
1044+
)
1045+
1046+
# Check for user fields
1047+
if "@" in field_value or "user" in field_id.lower():
1048+
analysis["suggestions"].extend(
1049+
[
1050+
f"Try user object format: {{'name': '{field_value}'}}",
1051+
f"Try user object format: {{'accountId': '{field_value}'}}",
1052+
]
1053+
)
1054+
1055+
# Check for numeric values that might need string format
1056+
elif isinstance(field_value, int | float):
1057+
analysis["suggestions"].append(f"Try string format: '{field_value}'")
1058+
1059+
# Check for very large numbers that might be ranking/ordering fields
1060+
if isinstance(field_value, int) and field_value > 1000000000:
1061+
analysis["suggestions"].extend(
1062+
[
1063+
"This looks like a ranking/ordering field - consider if it should be auto-managed by JIRA",
1064+
"Try string format for large numbers",
1065+
]
1066+
)
1067+
1068+
return analysis
1069+
1070+
8691071
@jira_mcp.tool(tags={"jira", "write"})
8701072
@check_write_access
8711073
async def update_issue(
@@ -881,7 +1083,7 @@ async def update_issue(
8811083
),
8821084
],
8831085
additional_fields: Annotated[
884-
dict[str, Any] | None,
1086+
dict[str, Any] | str | None,
8851087
Field(
8861088
description="(Optional) Dictionary of additional fields to update. Use this for custom fields or more complex updates.",
8871089
default=None,
@@ -919,10 +1121,22 @@ async def update_issue(
9191121
raise ValueError("fields must be a dictionary.")
9201122
update_fields = fields
9211123

922-
# Use additional_fields directly as dict
923-
extra_fields = additional_fields or {}
924-
if not isinstance(extra_fields, dict):
925-
raise ValueError("additional_fields must be a dictionary.")
1124+
# Accept either dict or JSON string for additional fields
1125+
if additional_fields is None:
1126+
extra_fields: dict[str, Any] = {}
1127+
elif isinstance(additional_fields, dict):
1128+
extra_fields = additional_fields
1129+
elif isinstance(additional_fields, str):
1130+
try:
1131+
extra_fields = json.loads(additional_fields)
1132+
if not isinstance(extra_fields, dict):
1133+
raise ValueError(
1134+
"Parsed additional_fields is not a JSON object (dict)."
1135+
)
1136+
except json.JSONDecodeError as e:
1137+
raise ValueError(f"additional_fields is not valid JSON: {e}") from e
1138+
else:
1139+
raise ValueError("additional_fields must be a dictionary or JSON string.")
9261140

9271141
# Parse attachments
9281142
attachment_paths = []
@@ -963,8 +1177,101 @@ async def update_issue(
9631177
ensure_ascii=False,
9641178
)
9651179
except Exception as e:
966-
logger.error(f"Error updating issue {issue_key}: {str(e)}", exc_info=True)
967-
raise ValueError(f"Failed to update issue {issue_key}: {str(e)}")
1180+
# Enhanced error details for debugging
1181+
error_details = {
1182+
"error_type": type(e).__name__,
1183+
"message": str(e),
1184+
"issue_key": issue_key,
1185+
"attempted_updates": {
1186+
"fields": update_fields,
1187+
"additional_fields": extra_fields,
1188+
"attachment_paths": attachment_paths,
1189+
},
1190+
}
1191+
1192+
# Try to extract JIRA-specific error information
1193+
if hasattr(e, "response") and e.response is not None:
1194+
error_details.update(
1195+
{
1196+
"http_status": getattr(e.response, "status_code", None),
1197+
"jira_response": getattr(e.response, "text", None),
1198+
}
1199+
)
1200+
1201+
# Try to extract more detailed error info from common JIRA exception types
1202+
if hasattr(e, "status_code"):
1203+
error_details["http_status"] = e.status_code
1204+
if hasattr(e, "text"):
1205+
error_details["jira_response"] = e.text
1206+
1207+
logger.error(
1208+
f"Error updating issue {issue_key}: {json.dumps(error_details, indent=2)}",
1209+
exc_info=True,
1210+
)
1211+
1212+
# If we have additional fields and the batch update failed, try individual field updates
1213+
if extra_fields:
1214+
logger.info(
1215+
f"Batch update failed for {issue_key}, attempting individual field updates..."
1216+
)
1217+
1218+
# First try to update standard fields only (if any)
1219+
standard_update_success = False
1220+
if update_fields:
1221+
try:
1222+
jira.update_issue(issue_key=issue_key, **update_fields)
1223+
standard_update_success = True
1224+
logger.info(f"Standard fields updated successfully for {issue_key}")
1225+
except Exception as std_error:
1226+
logger.warning(
1227+
f"Standard fields also failed for {issue_key}: {str(std_error)}"
1228+
)
1229+
1230+
# Try individual additional field updates
1231+
individual_results = try_individual_field_updates(
1232+
jira, issue_key, extra_fields
1233+
)
1234+
1235+
# If some individual updates succeeded, return partial success
1236+
if individual_results["success_count"] > 0 or standard_update_success:
1237+
response = {
1238+
"message": "Partial update completed with some failures",
1239+
"issue_key": issue_key,
1240+
"batch_error": error_details,
1241+
"individual_field_results": individual_results,
1242+
"standard_fields_updated": standard_update_success,
1243+
}
1244+
1245+
# Try to get the updated issue for the response
1246+
try:
1247+
updated_issue = jira.get_issue(issue_key=issue_key)
1248+
response["issue"] = updated_issue.to_simplified_dict()
1249+
except Exception:
1250+
response["issue"] = {
1251+
"key": issue_key,
1252+
"note": "Could not retrieve updated issue details",
1253+
}
1254+
1255+
return json.dumps(response, indent=2, ensure_ascii=False)
1256+
1257+
# If no fallback succeeded or no additional fields, return structured error response
1258+
# instead of raising exception to ensure detailed error info reaches MCP client
1259+
error_response = {
1260+
"success": False,
1261+
"error": f"Failed to update issue {issue_key}",
1262+
"details": error_details,
1263+
"troubleshooting_guide": "Check field names, required fields, and permissions in your JIRA project",
1264+
}
1265+
1266+
# Add field analysis for debugging if we had extra fields
1267+
if extra_fields:
1268+
error_response["field_analysis"] = {}
1269+
for field_name, field_value in extra_fields.items():
1270+
error_response["field_analysis"][field_name] = analyze_field_format(
1271+
field_name, field_value
1272+
)
1273+
1274+
return json.dumps(error_response, indent=2, ensure_ascii=False)
9681275

9691276

9701277
@jira_mcp.tool(tags={"jira", "write"})

0 commit comments

Comments
 (0)