@@ -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 \n JIRA API Response:\n { json .dumps (jira_error , indent = 2 )} "
737+ )
738+ except Exception :
739+ error_msg += f"\n \n JIRA 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 \n Fallback creation also failed: { str (fallback_error )} "
798+
799+ # Add field format analysis for debugging
800+ if extra_fields :
801+ error_msg += "\n \n Field 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
8711073async 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