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.
- mcp_server/api_clients/github_client.py +1989 -543
- mcp_server/resources/github_resources.py +124 -0
- mcp_server/tools/github_tools.py +635 -1
- {quickcall_integrations-0.3.9.dist-info → quickcall_integrations-0.5.0.dist-info}/METADATA +46 -2
- {quickcall_integrations-0.3.9.dist-info → quickcall_integrations-0.5.0.dist-info}/RECORD +7 -7
- {quickcall_integrations-0.3.9.dist-info → quickcall_integrations-0.5.0.dist-info}/WHEEL +0 -0
- {quickcall_integrations-0.3.9.dist-info → quickcall_integrations-0.5.0.dist-info}/entry_points.txt +0 -0
mcp_server/tools/github_tools.py
CHANGED
|
@@ -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)}")
|