quickcall-integrations 0.3.9__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -575,7 +575,7 @@ def create_github_tools(mcp: FastMCP) -> None:
575
575
  ),
576
576
  issue_numbers: Optional[List[int]] = Field(
577
577
  default=None,
578
- description="Issue number(s). Required for view/update/close/reopen/comment/sub-issue ops.",
578
+ description="Issue number(s). Required for view/update/close/reopen/comment/sub-issue/project ops.",
579
579
  ),
580
580
  title: Optional[str] = Field(
581
581
  default=None,
@@ -647,6 +647,8 @@ def create_github_tools(mcp: FastMCP) -> None:
647
647
  - add sub-issues: manage_issues(action="add_sub_issue", issue_numbers=[43,44], parent_issue=42)
648
648
  - remove sub-issue: manage_issues(action="remove_sub_issue", issue_numbers=[43], parent_issue=42)
649
649
  - list sub-issues: manage_issues(action="list_sub_issues", parent_issue=42)
650
+
651
+ For project operations (add to project, update fields), use manage_projects() instead.
650
652
  """
651
653
  try:
652
654
  client = _get_client()
@@ -848,6 +850,404 @@ def create_github_tools(mcp: FastMCP) -> None:
848
850
  except Exception as e:
849
851
  raise ToolError(f"Failed to {action} issue(s): {str(e)}")
850
852
 
853
+ @mcp.tool(tags={"github", "prs"})
854
+ def manage_prs(
855
+ action: str = Field(
856
+ ...,
857
+ description="Action: 'list', 'view', 'create', 'update', 'merge', 'close', 'reopen', "
858
+ "'comment', 'request_reviewers', 'review', 'to_draft', 'ready_for_review', "
859
+ "'add_labels', 'remove_labels', 'add_assignees', 'remove_assignees'",
860
+ ),
861
+ pr_numbers: Optional[List[int]] = Field(
862
+ default=None,
863
+ description="PR number(s). Required for view/update/merge/close/reopen/comment/review/labels/assignees actions.",
864
+ ),
865
+ title: Optional[str] = Field(
866
+ default=None,
867
+ description="PR title (for 'create' or 'update')",
868
+ ),
869
+ body: Optional[str] = Field(
870
+ default=None,
871
+ description="PR body/description (for 'create'/'update') or comment text (for 'comment')",
872
+ ),
873
+ head: Optional[str] = Field(
874
+ default=None,
875
+ description="Source branch containing changes (for 'create')",
876
+ ),
877
+ base: Optional[str] = Field(
878
+ default=None,
879
+ description="Target branch to merge into (for 'create', default: repo default branch)",
880
+ ),
881
+ draft: Optional[bool] = Field(
882
+ default=None,
883
+ description="Create as draft PR (for 'create')",
884
+ ),
885
+ merge_method: Optional[str] = Field(
886
+ default="merge",
887
+ description="Merge strategy: 'merge', 'squash', or 'rebase' (for 'merge' action)",
888
+ ),
889
+ reviewers: Optional[List[str]] = Field(
890
+ default=None,
891
+ description="GitHub usernames to request review from (for 'request_reviewers')",
892
+ ),
893
+ team_reviewers: Optional[List[str]] = Field(
894
+ default=None,
895
+ description="Team slugs to request review from (for 'request_reviewers')",
896
+ ),
897
+ review_event: Optional[str] = Field(
898
+ default=None,
899
+ description="Review event: 'APPROVE', 'REQUEST_CHANGES', or 'COMMENT' (for 'review' action)",
900
+ ),
901
+ labels: Optional[List[str]] = Field(
902
+ default=None,
903
+ description="Labels (for 'add_labels' or 'remove_labels')",
904
+ ),
905
+ assignees: Optional[List[str]] = Field(
906
+ default=None,
907
+ description="GitHub usernames for assignees. For 'create': defaults to self if not specified. "
908
+ "For 'add_assignees'/'remove_assignees': required.",
909
+ ),
910
+ owner: Optional[str] = Field(
911
+ default=None,
912
+ description="Repository owner",
913
+ ),
914
+ repo: Optional[str] = Field(
915
+ default=None,
916
+ description="Repository name. Required.",
917
+ ),
918
+ state: Optional[str] = Field(
919
+ default="open",
920
+ description="PR state filter for 'list': 'open', 'closed', or 'all' (default: 'open')",
921
+ ),
922
+ limit: Optional[int] = Field(
923
+ default=20,
924
+ description="Maximum PRs to return for 'list' action (default: 20)",
925
+ ),
926
+ ) -> dict:
927
+ """
928
+ Manage GitHub pull requests: list, view, create, update, merge, close, reopen, comment, and review.
929
+
930
+ Supports bulk operations for view/close/reopen/comment via pr_numbers list.
931
+
932
+ Examples:
933
+ - list: manage_prs(action="list", state="open")
934
+ - view: manage_prs(action="view", pr_numbers=[42])
935
+ - create: manage_prs(action="create", title="Feature X", head="feature-branch", base="main")
936
+ - create draft: manage_prs(action="create", title="WIP", head="wip-branch", draft=True)
937
+ - update: manage_prs(action="update", pr_numbers=[42], title="New title", body="Updated desc")
938
+ - merge: manage_prs(action="merge", pr_numbers=[42], merge_method="squash")
939
+ - close: manage_prs(action="close", pr_numbers=[42])
940
+ - reopen: manage_prs(action="reopen", pr_numbers=[42])
941
+ - comment: manage_prs(action="comment", pr_numbers=[42], body="LGTM!")
942
+ - request review: manage_prs(action="request_reviewers", pr_numbers=[42], reviewers=["user1"])
943
+ - approve: manage_prs(action="review", pr_numbers=[42], review_event="APPROVE", body="Looks good!")
944
+ - to draft: manage_prs(action="to_draft", pr_numbers=[42])
945
+ - ready: manage_prs(action="ready_for_review", pr_numbers=[42])
946
+ - add labels: manage_prs(action="add_labels", pr_numbers=[42], labels=["bug", "urgent"])
947
+ - remove labels: manage_prs(action="remove_labels", pr_numbers=[42], labels=["wip"])
948
+ - add assignees: manage_prs(action="add_assignees", pr_numbers=[42], assignees=["user1"])
949
+ - remove assignees: manage_prs(action="remove_assignees", pr_numbers=[42], assignees=["user1"])
950
+ """
951
+ try:
952
+ client = _get_client()
953
+
954
+ # === LIST ACTION ===
955
+ if action == "list":
956
+ prs = client.list_prs(
957
+ owner=owner,
958
+ repo=repo,
959
+ state=state or "open",
960
+ limit=limit or 20,
961
+ detail_level="summary",
962
+ )
963
+ return {
964
+ "action": "list",
965
+ "state": state or "open",
966
+ "count": len(prs),
967
+ "prs": [pr.model_dump() for pr in prs],
968
+ }
969
+
970
+ # === CREATE ACTION ===
971
+ if action == "create":
972
+ if not title:
973
+ raise ToolError("'title' is required for 'create' action")
974
+ if not head:
975
+ raise ToolError(
976
+ "'head' (source branch) is required for 'create' action"
977
+ )
978
+
979
+ pr = client.create_pr(
980
+ title=title,
981
+ head=head,
982
+ base=base or "main",
983
+ body=body,
984
+ draft=draft or False,
985
+ owner=owner,
986
+ repo=repo,
987
+ )
988
+
989
+ result = {
990
+ "action": "created",
991
+ "pr": pr.model_dump(),
992
+ }
993
+
994
+ # Auto-assign to self if assignees not specified
995
+ pr_assignees = assignees
996
+ if pr_assignees is None:
997
+ # Default to current user
998
+ current_user = client.get_authenticated_user()
999
+ if current_user:
1000
+ pr_assignees = [current_user]
1001
+
1002
+ if pr_assignees:
1003
+ try:
1004
+ assign_result = client.add_pr_assignees(
1005
+ pr.number, assignees=pr_assignees, owner=owner, repo=repo
1006
+ )
1007
+ result["assignees"] = assign_result["assignees"]
1008
+ except Exception as e:
1009
+ result["assignee_error"] = str(e)
1010
+
1011
+ return result
1012
+
1013
+ # === ALL OTHER ACTIONS REQUIRE pr_numbers ===
1014
+ if not pr_numbers:
1015
+ raise ToolError(f"'pr_numbers' required for '{action}' action")
1016
+
1017
+ results = []
1018
+ for pr_number in pr_numbers:
1019
+ # === VIEW ACTION ===
1020
+ if action == "view":
1021
+ pr_data = client.get_pr(
1022
+ pr_number=pr_number,
1023
+ owner=owner,
1024
+ repo=repo,
1025
+ )
1026
+ if pr_data:
1027
+ results.append(pr_data.model_dump())
1028
+ else:
1029
+ results.append({"number": pr_number, "error": "PR not found"})
1030
+
1031
+ # === UPDATE ACTION ===
1032
+ elif action == "update":
1033
+ pr_data = client.update_pr(
1034
+ pr_number=pr_number,
1035
+ title=title,
1036
+ body=body,
1037
+ base=base,
1038
+ owner=owner,
1039
+ repo=repo,
1040
+ )
1041
+ results.append(
1042
+ {
1043
+ "number": pr_number,
1044
+ "status": "updated",
1045
+ "pr": pr_data.model_dump(),
1046
+ }
1047
+ )
1048
+
1049
+ # === MERGE ACTION ===
1050
+ elif action == "merge":
1051
+ merge_result = client.merge_pr(
1052
+ pr_number=pr_number,
1053
+ merge_method=merge_method or "merge",
1054
+ owner=owner,
1055
+ repo=repo,
1056
+ )
1057
+ results.append(
1058
+ {
1059
+ "number": pr_number,
1060
+ "status": "merged" if merge_result["merged"] else "failed",
1061
+ "message": merge_result["message"],
1062
+ "sha": merge_result["sha"],
1063
+ }
1064
+ )
1065
+
1066
+ # === CLOSE ACTION ===
1067
+ elif action == "close":
1068
+ client.close_pr(pr_number, owner=owner, repo=repo)
1069
+ results.append({"number": pr_number, "status": "closed"})
1070
+
1071
+ # === REOPEN ACTION ===
1072
+ elif action == "reopen":
1073
+ client.reopen_pr(pr_number, owner=owner, repo=repo)
1074
+ results.append({"number": pr_number, "status": "reopened"})
1075
+
1076
+ # === COMMENT ACTION ===
1077
+ elif action == "comment":
1078
+ if not body:
1079
+ raise ToolError("'body' is required for 'comment' action")
1080
+ comment = client.add_pr_comment(
1081
+ pr_number, body=body, owner=owner, repo=repo
1082
+ )
1083
+ results.append(
1084
+ {
1085
+ "number": pr_number,
1086
+ "status": "commented",
1087
+ "comment_url": comment["html_url"],
1088
+ }
1089
+ )
1090
+
1091
+ # === REQUEST REVIEWERS ACTION ===
1092
+ elif action == "request_reviewers":
1093
+ if not reviewers and not team_reviewers:
1094
+ raise ToolError(
1095
+ "'reviewers' or 'team_reviewers' required for 'request_reviewers' action"
1096
+ )
1097
+ review_result = client.request_reviewers(
1098
+ pr_number,
1099
+ reviewers=reviewers,
1100
+ team_reviewers=team_reviewers,
1101
+ owner=owner,
1102
+ repo=repo,
1103
+ )
1104
+ results.append(
1105
+ {
1106
+ "number": pr_number,
1107
+ "status": "reviewers_requested",
1108
+ "requested_reviewers": review_result["requested_reviewers"],
1109
+ "requested_teams": review_result["requested_teams"],
1110
+ }
1111
+ )
1112
+
1113
+ # === REVIEW ACTION ===
1114
+ elif action == "review":
1115
+ if not review_event:
1116
+ raise ToolError(
1117
+ "'review_event' (APPROVE, REQUEST_CHANGES, COMMENT) required for 'review' action"
1118
+ )
1119
+ if review_event == "REQUEST_CHANGES" and not body:
1120
+ raise ToolError("'body' is required when requesting changes")
1121
+ review_result = client.submit_pr_review(
1122
+ pr_number,
1123
+ event=review_event,
1124
+ body=body,
1125
+ owner=owner,
1126
+ repo=repo,
1127
+ )
1128
+ results.append(
1129
+ {
1130
+ "number": pr_number,
1131
+ "status": "reviewed",
1132
+ "review_state": review_result["state"],
1133
+ "review_url": review_result["html_url"],
1134
+ }
1135
+ )
1136
+
1137
+ # === TO DRAFT ACTION ===
1138
+ elif action == "to_draft":
1139
+ draft_result = client.convert_pr_to_draft(
1140
+ pr_number, owner=owner, repo=repo
1141
+ )
1142
+ results.append(
1143
+ {
1144
+ "number": pr_number,
1145
+ "status": "converted_to_draft",
1146
+ "is_draft": draft_result["is_draft"],
1147
+ "message": draft_result["message"],
1148
+ }
1149
+ )
1150
+
1151
+ # === READY FOR REVIEW ACTION ===
1152
+ elif action == "ready_for_review":
1153
+ ready_result = client.mark_pr_ready_for_review(
1154
+ pr_number, owner=owner, repo=repo
1155
+ )
1156
+ results.append(
1157
+ {
1158
+ "number": pr_number,
1159
+ "status": "marked_ready",
1160
+ "is_draft": ready_result["is_draft"],
1161
+ "message": ready_result["message"],
1162
+ }
1163
+ )
1164
+
1165
+ # === ADD LABELS ACTION ===
1166
+ elif action == "add_labels":
1167
+ if not labels:
1168
+ raise ToolError("'labels' is required for 'add_labels' action")
1169
+ label_result = client.add_pr_labels(
1170
+ pr_number, labels=labels, owner=owner, repo=repo
1171
+ )
1172
+ results.append(
1173
+ {
1174
+ "number": pr_number,
1175
+ "status": "labels_added",
1176
+ "labels": label_result["labels"],
1177
+ }
1178
+ )
1179
+
1180
+ # === REMOVE LABELS ACTION ===
1181
+ elif action == "remove_labels":
1182
+ if not labels:
1183
+ raise ToolError(
1184
+ "'labels' is required for 'remove_labels' action"
1185
+ )
1186
+ label_result = client.remove_pr_labels(
1187
+ pr_number, labels=labels, owner=owner, repo=repo
1188
+ )
1189
+ results.append(
1190
+ {
1191
+ "number": pr_number,
1192
+ "status": "labels_removed",
1193
+ "labels": label_result["labels"],
1194
+ }
1195
+ )
1196
+
1197
+ # === ADD ASSIGNEES ACTION ===
1198
+ elif action == "add_assignees":
1199
+ if not assignees:
1200
+ raise ToolError(
1201
+ "'assignees' is required for 'add_assignees' action"
1202
+ )
1203
+ assign_result = client.add_pr_assignees(
1204
+ pr_number, assignees=assignees, owner=owner, repo=repo
1205
+ )
1206
+ results.append(
1207
+ {
1208
+ "number": pr_number,
1209
+ "status": "assignees_added",
1210
+ "assignees": assign_result["assignees"],
1211
+ }
1212
+ )
1213
+
1214
+ # === REMOVE ASSIGNEES ACTION ===
1215
+ elif action == "remove_assignees":
1216
+ if not assignees:
1217
+ raise ToolError(
1218
+ "'assignees' is required for 'remove_assignees' action"
1219
+ )
1220
+ assign_result = client.remove_pr_assignees(
1221
+ pr_number, assignees=assignees, owner=owner, repo=repo
1222
+ )
1223
+ results.append(
1224
+ {
1225
+ "number": pr_number,
1226
+ "status": "assignees_removed",
1227
+ "assignees": assign_result["assignees"],
1228
+ }
1229
+ )
1230
+
1231
+ else:
1232
+ raise ToolError(f"Invalid action: {action}")
1233
+
1234
+ # Return format depends on action
1235
+ if action == "view":
1236
+ return {
1237
+ "action": "view",
1238
+ "count": len(results),
1239
+ "prs": results,
1240
+ }
1241
+
1242
+ return {"action": action, "count": len(results), "results": results}
1243
+
1244
+ except ToolError:
1245
+ raise
1246
+ except ValueError as e:
1247
+ raise ToolError(f"Repository not specified: {str(e)}")
1248
+ except Exception as e:
1249
+ raise ToolError(f"Failed to {action} PR(s): {str(e)}")
1250
+
851
1251
  @mcp.tool(tags={"github", "prs", "appraisal"})
852
1252
  def prepare_appraisal_data(
853
1253
  author: Optional[str] = Field(
@@ -1091,3 +1491,237 @@ def create_github_tools(mcp: FastMCP) -> None:
1091
1491
  "connected": False,
1092
1492
  "error": str(e),
1093
1493
  }
1494
+
1495
+ @mcp.tool(tags={"github", "projects"})
1496
+ def manage_projects(
1497
+ action: str = Field(
1498
+ ...,
1499
+ description="Action: 'list', 'add', 'remove', 'update_fields'",
1500
+ ),
1501
+ issue_numbers: Optional[List[int]] = Field(
1502
+ default=None,
1503
+ description="Issue number(s) for add/remove/update_fields actions.",
1504
+ ),
1505
+ project: Optional[str] = Field(
1506
+ default=None,
1507
+ description="Project number or title. Required for add/remove/update_fields.",
1508
+ ),
1509
+ fields: Optional[Dict[str, str]] = Field(
1510
+ default=None,
1511
+ description="Dict of field names to values for 'add' or 'update_fields'. "
1512
+ "Example: {'Status': 'In Progress', 'Priority': 'High'}",
1513
+ ),
1514
+ owner: Optional[str] = Field(
1515
+ default=None,
1516
+ description="Repository owner (for the issues).",
1517
+ ),
1518
+ repo: Optional[str] = Field(
1519
+ default=None,
1520
+ description="Repository name (for the issues).",
1521
+ ),
1522
+ project_owner: Optional[str] = Field(
1523
+ default=None,
1524
+ description="Owner of the project (org or user). Defaults to repo owner.",
1525
+ ),
1526
+ limit: Optional[int] = Field(
1527
+ default=20,
1528
+ description="Maximum projects to return for 'list' action.",
1529
+ ),
1530
+ ) -> dict:
1531
+ """
1532
+ Manage GitHub Projects V2: list projects, add/remove issues, update fields.
1533
+
1534
+ IMPORTANT: Use 'add' with 'fields' to add issue AND set fields in ONE call.
1535
+ Don't make separate calls for add + update_fields.
1536
+
1537
+ Examples:
1538
+ - list: manage_projects(action="list", owner="org-name")
1539
+ - add with fields: manage_projects(action="add", issue_numbers=[42], project="1",
1540
+ fields={"Status": "Triage", "Priority": "High"}, repo="my-repo")
1541
+ - add only: manage_projects(action="add", issue_numbers=[42], project="1", repo="my-repo")
1542
+ - remove: manage_projects(action="remove", issue_numbers=[42], project="1", repo="my-repo")
1543
+ - update fields: manage_projects(action="update_fields", issue_numbers=[42], project="1",
1544
+ fields={"Status": "In Progress"}, repo="my-repo")
1545
+ """
1546
+ try:
1547
+ client = _get_client()
1548
+
1549
+ # === LIST ACTION ===
1550
+ if action == "list":
1551
+ proj_owner = project_owner or owner
1552
+ if not proj_owner:
1553
+ # Use authenticated user if no owner specified
1554
+ proj_owner = client.get_authenticated_user()
1555
+
1556
+ projects = client.list_projects(
1557
+ owner=proj_owner,
1558
+ is_org=True, # Try org first, falls back to user
1559
+ limit=limit or 20,
1560
+ )
1561
+ return {
1562
+ "action": "list",
1563
+ "owner": proj_owner,
1564
+ "count": len(projects),
1565
+ "projects": projects,
1566
+ }
1567
+
1568
+ # === ADD ACTION ===
1569
+ if action == "add":
1570
+ if not project:
1571
+ raise ToolError("'project' is required for 'add' action")
1572
+ if not issue_numbers:
1573
+ raise ToolError("'issue_numbers' is required for 'add' action")
1574
+
1575
+ results = []
1576
+ for issue_number in issue_numbers:
1577
+ issue_result = {
1578
+ "number": issue_number,
1579
+ "project": project,
1580
+ }
1581
+ try:
1582
+ result = client.add_issue_to_project(
1583
+ issue_number=issue_number,
1584
+ project=project,
1585
+ owner=owner,
1586
+ repo=repo,
1587
+ project_owner=project_owner,
1588
+ )
1589
+ issue_result["status"] = "added"
1590
+ issue_result["project_item_id"] = result.get("project_item_id")
1591
+
1592
+ # If fields provided, set them after adding
1593
+ if fields:
1594
+ issue_result["fields_updated"] = []
1595
+ issue_result["field_errors"] = []
1596
+ for field_name, value in fields.items():
1597
+ try:
1598
+ client.update_project_item_field(
1599
+ issue_number=issue_number,
1600
+ project=project,
1601
+ field_name=field_name,
1602
+ value=value,
1603
+ owner=owner,
1604
+ repo=repo,
1605
+ project_owner=project_owner,
1606
+ )
1607
+ issue_result["fields_updated"].append(
1608
+ {"field": field_name, "value": value}
1609
+ )
1610
+ except Exception as e:
1611
+ issue_result["field_errors"].append(
1612
+ {"field": field_name, "error": str(e)}
1613
+ )
1614
+
1615
+ results.append(issue_result)
1616
+ except Exception as e:
1617
+ issue_result["status"] = "error"
1618
+ issue_result["error"] = str(e)
1619
+ results.append(issue_result)
1620
+
1621
+ return {
1622
+ "action": "add",
1623
+ "project": project,
1624
+ "count": len(results),
1625
+ "results": results,
1626
+ }
1627
+
1628
+ # === REMOVE ACTION ===
1629
+ if action == "remove":
1630
+ if not project:
1631
+ raise ToolError("'project' is required for 'remove' action")
1632
+ if not issue_numbers:
1633
+ raise ToolError("'issue_numbers' is required for 'remove' action")
1634
+
1635
+ results = []
1636
+ for issue_number in issue_numbers:
1637
+ try:
1638
+ client.remove_issue_from_project(
1639
+ issue_number=issue_number,
1640
+ project=project,
1641
+ owner=owner,
1642
+ repo=repo,
1643
+ project_owner=project_owner,
1644
+ )
1645
+ results.append(
1646
+ {
1647
+ "number": issue_number,
1648
+ "status": "removed",
1649
+ "project": project,
1650
+ }
1651
+ )
1652
+ except Exception as e:
1653
+ results.append(
1654
+ {
1655
+ "number": issue_number,
1656
+ "status": "error",
1657
+ "error": str(e),
1658
+ }
1659
+ )
1660
+
1661
+ return {
1662
+ "action": "remove",
1663
+ "project": project,
1664
+ "count": len(results),
1665
+ "results": results,
1666
+ }
1667
+
1668
+ # === UPDATE FIELDS ACTION ===
1669
+ if action == "update_fields":
1670
+ if not project:
1671
+ raise ToolError("'project' is required for 'update_fields' action")
1672
+ if not issue_numbers:
1673
+ raise ToolError(
1674
+ "'issue_numbers' is required for 'update_fields' action"
1675
+ )
1676
+ if not fields:
1677
+ raise ToolError("'fields' is required for 'update_fields' action")
1678
+
1679
+ results = []
1680
+ for issue_number in issue_numbers:
1681
+ issue_result = {
1682
+ "number": issue_number,
1683
+ "fields_updated": [],
1684
+ "errors": [],
1685
+ }
1686
+
1687
+ for field_name, value in fields.items():
1688
+ try:
1689
+ client.update_project_item_field(
1690
+ issue_number=issue_number,
1691
+ project=project,
1692
+ field_name=field_name,
1693
+ value=value,
1694
+ owner=owner,
1695
+ repo=repo,
1696
+ project_owner=project_owner,
1697
+ )
1698
+ issue_result["fields_updated"].append(
1699
+ {"field": field_name, "value": value}
1700
+ )
1701
+ except Exception as e:
1702
+ issue_result["errors"].append(
1703
+ {"field": field_name, "error": str(e)}
1704
+ )
1705
+
1706
+ issue_result["status"] = (
1707
+ "success" if not issue_result["errors"] else "partial"
1708
+ )
1709
+ results.append(issue_result)
1710
+
1711
+ return {
1712
+ "action": "update_fields",
1713
+ "project": project,
1714
+ "count": len(results),
1715
+ "results": results,
1716
+ }
1717
+
1718
+ raise ToolError(
1719
+ f"Invalid action: {action}. Valid actions: list, add, remove, update_fields"
1720
+ )
1721
+
1722
+ except ToolError:
1723
+ raise
1724
+ except ValueError as e:
1725
+ raise ToolError(f"Invalid parameters: {str(e)}")
1726
+ except Exception as e:
1727
+ raise ToolError(f"Failed to {action} project: {str(e)}")