quickcall-integrations 0.4.0__py3-none-any.whl → 0.6.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 +682 -0
- mcp_server/tools/github_tools.py +484 -2
- {quickcall_integrations-0.4.0.dist-info → quickcall_integrations-0.6.0.dist-info}/METADATA +64 -2
- {quickcall_integrations-0.4.0.dist-info → quickcall_integrations-0.6.0.dist-info}/RECORD +6 -6
- {quickcall_integrations-0.4.0.dist-info → quickcall_integrations-0.6.0.dist-info}/WHEEL +0 -0
- {quickcall_integrations-0.4.0.dist-info → quickcall_integrations-0.6.0.dist-info}/entry_points.txt +0 -0
|
@@ -417,6 +417,549 @@ class GitHubClient:
|
|
|
417
417
|
html_url=pr.html_url,
|
|
418
418
|
)
|
|
419
419
|
|
|
420
|
+
def create_pr(
|
|
421
|
+
self,
|
|
422
|
+
title: str,
|
|
423
|
+
head: str,
|
|
424
|
+
base: str,
|
|
425
|
+
body: Optional[str] = None,
|
|
426
|
+
draft: bool = False,
|
|
427
|
+
owner: Optional[str] = None,
|
|
428
|
+
repo: Optional[str] = None,
|
|
429
|
+
) -> PullRequest:
|
|
430
|
+
"""
|
|
431
|
+
Create a new pull request.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
title: PR title
|
|
435
|
+
head: Branch containing changes (e.g., "feature-branch")
|
|
436
|
+
base: Branch to merge into (e.g., "main")
|
|
437
|
+
body: PR description
|
|
438
|
+
draft: Create as draft PR
|
|
439
|
+
owner: Repository owner
|
|
440
|
+
repo: Repository name
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Created PullRequest
|
|
444
|
+
"""
|
|
445
|
+
gh_repo = self._get_repo(owner, repo)
|
|
446
|
+
pr = gh_repo.create_pull(
|
|
447
|
+
title=title,
|
|
448
|
+
head=head,
|
|
449
|
+
base=base,
|
|
450
|
+
body=body or "",
|
|
451
|
+
draft=draft,
|
|
452
|
+
)
|
|
453
|
+
return self._convert_pr(pr)
|
|
454
|
+
|
|
455
|
+
def update_pr(
|
|
456
|
+
self,
|
|
457
|
+
pr_number: int,
|
|
458
|
+
title: Optional[str] = None,
|
|
459
|
+
body: Optional[str] = None,
|
|
460
|
+
state: Optional[str] = None,
|
|
461
|
+
base: Optional[str] = None,
|
|
462
|
+
owner: Optional[str] = None,
|
|
463
|
+
repo: Optional[str] = None,
|
|
464
|
+
) -> PullRequest:
|
|
465
|
+
"""
|
|
466
|
+
Update an existing pull request.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
pr_number: PR number
|
|
470
|
+
title: New title
|
|
471
|
+
body: New description
|
|
472
|
+
state: New state ('open' or 'closed')
|
|
473
|
+
base: New base branch
|
|
474
|
+
owner: Repository owner
|
|
475
|
+
repo: Repository name
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
Updated PullRequest
|
|
479
|
+
"""
|
|
480
|
+
gh_repo = self._get_repo(owner, repo)
|
|
481
|
+
pr = gh_repo.get_pull(pr_number)
|
|
482
|
+
|
|
483
|
+
kwargs = {}
|
|
484
|
+
if title is not None:
|
|
485
|
+
kwargs["title"] = title
|
|
486
|
+
if body is not None:
|
|
487
|
+
kwargs["body"] = body
|
|
488
|
+
if state is not None:
|
|
489
|
+
kwargs["state"] = state
|
|
490
|
+
if base is not None:
|
|
491
|
+
kwargs["base"] = base
|
|
492
|
+
|
|
493
|
+
if kwargs:
|
|
494
|
+
pr.edit(**kwargs)
|
|
495
|
+
|
|
496
|
+
return self._convert_pr(pr)
|
|
497
|
+
|
|
498
|
+
def merge_pr(
|
|
499
|
+
self,
|
|
500
|
+
pr_number: int,
|
|
501
|
+
commit_title: Optional[str] = None,
|
|
502
|
+
commit_message: Optional[str] = None,
|
|
503
|
+
merge_method: str = "merge",
|
|
504
|
+
owner: Optional[str] = None,
|
|
505
|
+
repo: Optional[str] = None,
|
|
506
|
+
) -> Dict[str, Any]:
|
|
507
|
+
"""
|
|
508
|
+
Merge a pull request.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
pr_number: PR number
|
|
512
|
+
commit_title: Custom commit title (for squash/merge)
|
|
513
|
+
commit_message: Custom commit message
|
|
514
|
+
merge_method: 'merge', 'squash', or 'rebase'
|
|
515
|
+
owner: Repository owner
|
|
516
|
+
repo: Repository name
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
Dict with merge status and SHA
|
|
520
|
+
"""
|
|
521
|
+
gh_repo = self._get_repo(owner, repo)
|
|
522
|
+
pr = gh_repo.get_pull(pr_number)
|
|
523
|
+
|
|
524
|
+
# Check if PR is mergeable
|
|
525
|
+
if pr.merged:
|
|
526
|
+
return {
|
|
527
|
+
"merged": True,
|
|
528
|
+
"message": "PR was already merged",
|
|
529
|
+
"sha": pr.merge_commit_sha,
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if not pr.mergeable:
|
|
533
|
+
return {
|
|
534
|
+
"merged": False,
|
|
535
|
+
"message": "PR is not mergeable (conflicts or checks failing)",
|
|
536
|
+
"sha": None,
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
# Merge the PR
|
|
540
|
+
result = pr.merge(
|
|
541
|
+
commit_title=commit_title,
|
|
542
|
+
commit_message=commit_message,
|
|
543
|
+
merge_method=merge_method,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
"merged": result.merged,
|
|
548
|
+
"message": result.message,
|
|
549
|
+
"sha": result.sha,
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
def close_pr(
|
|
553
|
+
self,
|
|
554
|
+
pr_number: int,
|
|
555
|
+
owner: Optional[str] = None,
|
|
556
|
+
repo: Optional[str] = None,
|
|
557
|
+
) -> PullRequest:
|
|
558
|
+
"""Close a pull request without merging."""
|
|
559
|
+
gh_repo = self._get_repo(owner, repo)
|
|
560
|
+
pr = gh_repo.get_pull(pr_number)
|
|
561
|
+
pr.edit(state="closed")
|
|
562
|
+
return self._convert_pr(pr)
|
|
563
|
+
|
|
564
|
+
def reopen_pr(
|
|
565
|
+
self,
|
|
566
|
+
pr_number: int,
|
|
567
|
+
owner: Optional[str] = None,
|
|
568
|
+
repo: Optional[str] = None,
|
|
569
|
+
) -> PullRequest:
|
|
570
|
+
"""Reopen a closed pull request."""
|
|
571
|
+
gh_repo = self._get_repo(owner, repo)
|
|
572
|
+
pr = gh_repo.get_pull(pr_number)
|
|
573
|
+
pr.edit(state="open")
|
|
574
|
+
return self._convert_pr(pr)
|
|
575
|
+
|
|
576
|
+
def add_pr_comment(
|
|
577
|
+
self,
|
|
578
|
+
pr_number: int,
|
|
579
|
+
body: str,
|
|
580
|
+
owner: Optional[str] = None,
|
|
581
|
+
repo: Optional[str] = None,
|
|
582
|
+
) -> Dict[str, Any]:
|
|
583
|
+
"""
|
|
584
|
+
Add a comment to a pull request.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
pr_number: PR number
|
|
588
|
+
body: Comment text
|
|
589
|
+
owner: Repository owner
|
|
590
|
+
repo: Repository name
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
Comment details
|
|
594
|
+
"""
|
|
595
|
+
gh_repo = self._get_repo(owner, repo)
|
|
596
|
+
# PRs use the issue comment API
|
|
597
|
+
issue = gh_repo.get_issue(pr_number)
|
|
598
|
+
comment = issue.create_comment(body)
|
|
599
|
+
return {
|
|
600
|
+
"id": comment.id,
|
|
601
|
+
"body": comment.body,
|
|
602
|
+
"html_url": comment.html_url,
|
|
603
|
+
"created_at": comment.created_at.isoformat(),
|
|
604
|
+
"pr_number": pr_number,
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
def request_reviewers(
|
|
608
|
+
self,
|
|
609
|
+
pr_number: int,
|
|
610
|
+
reviewers: Optional[List[str]] = None,
|
|
611
|
+
team_reviewers: Optional[List[str]] = None,
|
|
612
|
+
owner: Optional[str] = None,
|
|
613
|
+
repo: Optional[str] = None,
|
|
614
|
+
) -> Dict[str, Any]:
|
|
615
|
+
"""
|
|
616
|
+
Request reviewers for a pull request.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
pr_number: PR number
|
|
620
|
+
reviewers: List of GitHub usernames
|
|
621
|
+
team_reviewers: List of team slugs (org teams)
|
|
622
|
+
owner: Repository owner
|
|
623
|
+
repo: Repository name
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
Dict with requested reviewers
|
|
627
|
+
"""
|
|
628
|
+
gh_repo = self._get_repo(owner, repo)
|
|
629
|
+
pr = gh_repo.get_pull(pr_number)
|
|
630
|
+
|
|
631
|
+
# Create review request
|
|
632
|
+
pr.create_review_request(
|
|
633
|
+
reviewers=reviewers or [],
|
|
634
|
+
team_reviewers=team_reviewers or [],
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
# Re-fetch to get updated reviewers
|
|
638
|
+
pr = gh_repo.get_pull(pr_number)
|
|
639
|
+
return {
|
|
640
|
+
"pr_number": pr_number,
|
|
641
|
+
"requested_reviewers": [r.login for r in pr.requested_reviewers],
|
|
642
|
+
"requested_teams": [t.slug for t in pr.requested_teams],
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
def submit_pr_review(
|
|
646
|
+
self,
|
|
647
|
+
pr_number: int,
|
|
648
|
+
event: str,
|
|
649
|
+
body: Optional[str] = None,
|
|
650
|
+
owner: Optional[str] = None,
|
|
651
|
+
repo: Optional[str] = None,
|
|
652
|
+
) -> Dict[str, Any]:
|
|
653
|
+
"""
|
|
654
|
+
Submit a review on a pull request.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
pr_number: PR number
|
|
658
|
+
event: Review event - 'APPROVE', 'REQUEST_CHANGES', or 'COMMENT'
|
|
659
|
+
body: Review comment (required for REQUEST_CHANGES)
|
|
660
|
+
owner: Repository owner
|
|
661
|
+
repo: Repository name
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
Review details
|
|
665
|
+
"""
|
|
666
|
+
gh_repo = self._get_repo(owner, repo)
|
|
667
|
+
pr = gh_repo.get_pull(pr_number)
|
|
668
|
+
|
|
669
|
+
review = pr.create_review(
|
|
670
|
+
body=body or "",
|
|
671
|
+
event=event,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
"id": review.id,
|
|
676
|
+
"state": review.state,
|
|
677
|
+
"body": review.body,
|
|
678
|
+
"html_url": review.html_url,
|
|
679
|
+
"pr_number": pr_number,
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
def convert_pr_to_draft(
|
|
683
|
+
self,
|
|
684
|
+
pr_number: int,
|
|
685
|
+
owner: Optional[str] = None,
|
|
686
|
+
repo: Optional[str] = None,
|
|
687
|
+
) -> Dict[str, Any]:
|
|
688
|
+
"""
|
|
689
|
+
Convert a PR to draft status using GraphQL API.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
pr_number: PR number
|
|
693
|
+
owner: Repository owner
|
|
694
|
+
repo: Repository name
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
Dict with success status
|
|
698
|
+
"""
|
|
699
|
+
owner = owner or self.default_owner
|
|
700
|
+
repo = repo or self.default_repo
|
|
701
|
+
|
|
702
|
+
if not owner or not repo:
|
|
703
|
+
raise ValueError("Repository owner and name must be specified")
|
|
704
|
+
|
|
705
|
+
# Get PR node ID
|
|
706
|
+
query = """
|
|
707
|
+
query($owner: String!, $repo: String!, $number: Int!) {
|
|
708
|
+
repository(owner: $owner, name: $repo) {
|
|
709
|
+
pullRequest(number: $number) {
|
|
710
|
+
id
|
|
711
|
+
isDraft
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
"""
|
|
716
|
+
data = self._graphql_request(
|
|
717
|
+
query, {"owner": owner, "repo": repo, "number": pr_number}
|
|
718
|
+
)
|
|
719
|
+
repository = data.get("repository")
|
|
720
|
+
if not repository:
|
|
721
|
+
raise GithubException(
|
|
722
|
+
404, {"message": f"Repository {owner}/{repo} not found"}
|
|
723
|
+
)
|
|
724
|
+
pr_data = repository.get("pullRequest")
|
|
725
|
+
if not pr_data:
|
|
726
|
+
raise GithubException(404, {"message": f"PR #{pr_number} not found"})
|
|
727
|
+
|
|
728
|
+
if pr_data.get("isDraft"):
|
|
729
|
+
return {
|
|
730
|
+
"pr_number": pr_number,
|
|
731
|
+
"is_draft": True,
|
|
732
|
+
"message": "PR is already a draft",
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
pr_node_id = pr_data["id"]
|
|
736
|
+
|
|
737
|
+
# Convert to draft
|
|
738
|
+
mutation = """
|
|
739
|
+
mutation($pullRequestId: ID!) {
|
|
740
|
+
convertPullRequestToDraft(input: {pullRequestId: $pullRequestId}) {
|
|
741
|
+
pullRequest {
|
|
742
|
+
id
|
|
743
|
+
isDraft
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
"""
|
|
748
|
+
result = self._graphql_request(mutation, {"pullRequestId": pr_node_id})
|
|
749
|
+
converted = result.get("convertPullRequestToDraft", {}).get("pullRequest", {})
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
"pr_number": pr_number,
|
|
753
|
+
"is_draft": converted.get("isDraft", True),
|
|
754
|
+
"message": "PR converted to draft",
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
def mark_pr_ready_for_review(
|
|
758
|
+
self,
|
|
759
|
+
pr_number: int,
|
|
760
|
+
owner: Optional[str] = None,
|
|
761
|
+
repo: Optional[str] = None,
|
|
762
|
+
) -> Dict[str, Any]:
|
|
763
|
+
"""
|
|
764
|
+
Mark a draft PR as ready for review using GraphQL API.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
pr_number: PR number
|
|
768
|
+
owner: Repository owner
|
|
769
|
+
repo: Repository name
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
Dict with success status
|
|
773
|
+
"""
|
|
774
|
+
owner = owner or self.default_owner
|
|
775
|
+
repo = repo or self.default_repo
|
|
776
|
+
|
|
777
|
+
if not owner or not repo:
|
|
778
|
+
raise ValueError("Repository owner and name must be specified")
|
|
779
|
+
|
|
780
|
+
# Get PR node ID
|
|
781
|
+
query = """
|
|
782
|
+
query($owner: String!, $repo: String!, $number: Int!) {
|
|
783
|
+
repository(owner: $owner, name: $repo) {
|
|
784
|
+
pullRequest(number: $number) {
|
|
785
|
+
id
|
|
786
|
+
isDraft
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
"""
|
|
791
|
+
data = self._graphql_request(
|
|
792
|
+
query, {"owner": owner, "repo": repo, "number": pr_number}
|
|
793
|
+
)
|
|
794
|
+
repository = data.get("repository")
|
|
795
|
+
if not repository:
|
|
796
|
+
raise GithubException(
|
|
797
|
+
404, {"message": f"Repository {owner}/{repo} not found"}
|
|
798
|
+
)
|
|
799
|
+
pr_data = repository.get("pullRequest")
|
|
800
|
+
if not pr_data:
|
|
801
|
+
raise GithubException(404, {"message": f"PR #{pr_number} not found"})
|
|
802
|
+
|
|
803
|
+
if not pr_data.get("isDraft"):
|
|
804
|
+
return {
|
|
805
|
+
"pr_number": pr_number,
|
|
806
|
+
"is_draft": False,
|
|
807
|
+
"message": "PR is already ready for review",
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
pr_node_id = pr_data["id"]
|
|
811
|
+
|
|
812
|
+
# Mark ready for review
|
|
813
|
+
mutation = """
|
|
814
|
+
mutation($pullRequestId: ID!) {
|
|
815
|
+
markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) {
|
|
816
|
+
pullRequest {
|
|
817
|
+
id
|
|
818
|
+
isDraft
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
"""
|
|
823
|
+
result = self._graphql_request(mutation, {"pullRequestId": pr_node_id})
|
|
824
|
+
converted = result.get("markPullRequestReadyForReview", {}).get(
|
|
825
|
+
"pullRequest", {}
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
return {
|
|
829
|
+
"pr_number": pr_number,
|
|
830
|
+
"is_draft": converted.get("isDraft", False),
|
|
831
|
+
"message": "PR marked ready for review",
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
def add_pr_labels(
|
|
835
|
+
self,
|
|
836
|
+
pr_number: int,
|
|
837
|
+
labels: List[str],
|
|
838
|
+
owner: Optional[str] = None,
|
|
839
|
+
repo: Optional[str] = None,
|
|
840
|
+
) -> Dict[str, Any]:
|
|
841
|
+
"""
|
|
842
|
+
Add labels to a pull request.
|
|
843
|
+
|
|
844
|
+
Args:
|
|
845
|
+
pr_number: PR number
|
|
846
|
+
labels: List of label names
|
|
847
|
+
owner: Repository owner
|
|
848
|
+
repo: Repository name
|
|
849
|
+
|
|
850
|
+
Returns:
|
|
851
|
+
Dict with updated labels
|
|
852
|
+
"""
|
|
853
|
+
gh_repo = self._get_repo(owner, repo)
|
|
854
|
+
# PRs use the issue API for labels
|
|
855
|
+
issue = gh_repo.get_issue(pr_number)
|
|
856
|
+
issue.add_to_labels(*labels)
|
|
857
|
+
|
|
858
|
+
return {
|
|
859
|
+
"pr_number": pr_number,
|
|
860
|
+
"labels": [label.name for label in issue.labels],
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
def remove_pr_labels(
|
|
864
|
+
self,
|
|
865
|
+
pr_number: int,
|
|
866
|
+
labels: List[str],
|
|
867
|
+
owner: Optional[str] = None,
|
|
868
|
+
repo: Optional[str] = None,
|
|
869
|
+
) -> Dict[str, Any]:
|
|
870
|
+
"""
|
|
871
|
+
Remove labels from a pull request.
|
|
872
|
+
|
|
873
|
+
Args:
|
|
874
|
+
pr_number: PR number
|
|
875
|
+
labels: List of label names to remove
|
|
876
|
+
owner: Repository owner
|
|
877
|
+
repo: Repository name
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
Dict with updated labels
|
|
881
|
+
"""
|
|
882
|
+
gh_repo = self._get_repo(owner, repo)
|
|
883
|
+
issue = gh_repo.get_issue(pr_number)
|
|
884
|
+
|
|
885
|
+
for label in labels:
|
|
886
|
+
try:
|
|
887
|
+
issue.remove_from_labels(label)
|
|
888
|
+
except GithubException as e:
|
|
889
|
+
if e.status != 404: # Ignore if label not present
|
|
890
|
+
raise
|
|
891
|
+
|
|
892
|
+
# Re-fetch to get updated labels
|
|
893
|
+
issue = gh_repo.get_issue(pr_number)
|
|
894
|
+
return {
|
|
895
|
+
"pr_number": pr_number,
|
|
896
|
+
"labels": [label.name for label in issue.labels],
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
def add_pr_assignees(
|
|
900
|
+
self,
|
|
901
|
+
pr_number: int,
|
|
902
|
+
assignees: List[str],
|
|
903
|
+
owner: Optional[str] = None,
|
|
904
|
+
repo: Optional[str] = None,
|
|
905
|
+
) -> Dict[str, Any]:
|
|
906
|
+
"""
|
|
907
|
+
Add assignees to a pull request.
|
|
908
|
+
|
|
909
|
+
Args:
|
|
910
|
+
pr_number: PR number
|
|
911
|
+
assignees: List of GitHub usernames
|
|
912
|
+
owner: Repository owner
|
|
913
|
+
repo: Repository name
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
Dict with updated assignees
|
|
917
|
+
"""
|
|
918
|
+
gh_repo = self._get_repo(owner, repo)
|
|
919
|
+
issue = gh_repo.get_issue(pr_number)
|
|
920
|
+
issue.add_to_assignees(*assignees)
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
"pr_number": pr_number,
|
|
924
|
+
"assignees": [a.login for a in issue.assignees],
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
def remove_pr_assignees(
|
|
928
|
+
self,
|
|
929
|
+
pr_number: int,
|
|
930
|
+
assignees: List[str],
|
|
931
|
+
owner: Optional[str] = None,
|
|
932
|
+
repo: Optional[str] = None,
|
|
933
|
+
) -> Dict[str, Any]:
|
|
934
|
+
"""
|
|
935
|
+
Remove assignees from a pull request.
|
|
936
|
+
|
|
937
|
+
Args:
|
|
938
|
+
pr_number: PR number
|
|
939
|
+
assignees: List of GitHub usernames to remove
|
|
940
|
+
owner: Repository owner
|
|
941
|
+
repo: Repository name
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
Dict with updated assignees
|
|
945
|
+
"""
|
|
946
|
+
gh_repo = self._get_repo(owner, repo)
|
|
947
|
+
issue = gh_repo.get_issue(pr_number)
|
|
948
|
+
|
|
949
|
+
for assignee in assignees:
|
|
950
|
+
try:
|
|
951
|
+
issue.remove_from_assignees(assignee)
|
|
952
|
+
except GithubException as e:
|
|
953
|
+
if e.status != 404: # Ignore if not assigned
|
|
954
|
+
raise
|
|
955
|
+
|
|
956
|
+
# Re-fetch to get updated assignees
|
|
957
|
+
issue = gh_repo.get_issue(pr_number)
|
|
958
|
+
return {
|
|
959
|
+
"pr_number": pr_number,
|
|
960
|
+
"assignees": [a.login for a in issue.assignees],
|
|
961
|
+
}
|
|
962
|
+
|
|
420
963
|
# ========================================================================
|
|
421
964
|
# Commit Operations
|
|
422
965
|
# ========================================================================
|
|
@@ -758,6 +1301,145 @@ class GitHubClient:
|
|
|
758
1301
|
"issue_number": issue_number,
|
|
759
1302
|
}
|
|
760
1303
|
|
|
1304
|
+
def list_issue_comments(
|
|
1305
|
+
self,
|
|
1306
|
+
issue_number: int,
|
|
1307
|
+
owner: Optional[str] = None,
|
|
1308
|
+
repo: Optional[str] = None,
|
|
1309
|
+
limit: int = 10,
|
|
1310
|
+
order: str = "asc",
|
|
1311
|
+
) -> List[Dict[str, Any]]:
|
|
1312
|
+
"""
|
|
1313
|
+
List comments on a GitHub issue.
|
|
1314
|
+
|
|
1315
|
+
Args:
|
|
1316
|
+
issue_number: Issue number
|
|
1317
|
+
owner: Repository owner
|
|
1318
|
+
repo: Repository name
|
|
1319
|
+
limit: Maximum comments to return (default: 10)
|
|
1320
|
+
order: 'asc' for oldest first, 'desc' for newest first (default: 'asc')
|
|
1321
|
+
|
|
1322
|
+
Returns:
|
|
1323
|
+
List of comment dicts with id, body, author, timestamps, url
|
|
1324
|
+
"""
|
|
1325
|
+
gh_repo = self._get_repo(owner, repo)
|
|
1326
|
+
issue = gh_repo.get_issue(issue_number)
|
|
1327
|
+
|
|
1328
|
+
comments = []
|
|
1329
|
+
all_comments = list(issue.get_comments())
|
|
1330
|
+
|
|
1331
|
+
# Apply order
|
|
1332
|
+
if order == "desc":
|
|
1333
|
+
all_comments = all_comments[::-1]
|
|
1334
|
+
|
|
1335
|
+
# Apply limit
|
|
1336
|
+
for comment in all_comments[:limit]:
|
|
1337
|
+
comments.append(
|
|
1338
|
+
{
|
|
1339
|
+
"id": comment.id,
|
|
1340
|
+
"body": comment.body,
|
|
1341
|
+
"author": comment.user.login if comment.user else "unknown",
|
|
1342
|
+
"created_at": comment.created_at.isoformat(),
|
|
1343
|
+
"updated_at": comment.updated_at.isoformat()
|
|
1344
|
+
if comment.updated_at
|
|
1345
|
+
else None,
|
|
1346
|
+
"html_url": comment.html_url,
|
|
1347
|
+
}
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1350
|
+
return comments
|
|
1351
|
+
|
|
1352
|
+
def get_issue_comment(
|
|
1353
|
+
self,
|
|
1354
|
+
comment_id: int,
|
|
1355
|
+
owner: Optional[str] = None,
|
|
1356
|
+
repo: Optional[str] = None,
|
|
1357
|
+
) -> Dict[str, Any]:
|
|
1358
|
+
"""
|
|
1359
|
+
Get a specific comment by ID.
|
|
1360
|
+
|
|
1361
|
+
Args:
|
|
1362
|
+
comment_id: Comment ID
|
|
1363
|
+
owner: Repository owner
|
|
1364
|
+
repo: Repository name
|
|
1365
|
+
|
|
1366
|
+
Returns:
|
|
1367
|
+
Comment dict with id, body, author, timestamps, url
|
|
1368
|
+
"""
|
|
1369
|
+
gh_repo = self._get_repo(owner, repo)
|
|
1370
|
+
comment = gh_repo.get_issue_comment(comment_id)
|
|
1371
|
+
|
|
1372
|
+
return {
|
|
1373
|
+
"id": comment.id,
|
|
1374
|
+
"body": comment.body,
|
|
1375
|
+
"author": comment.user.login if comment.user else "unknown",
|
|
1376
|
+
"created_at": comment.created_at.isoformat(),
|
|
1377
|
+
"updated_at": comment.updated_at.isoformat()
|
|
1378
|
+
if comment.updated_at
|
|
1379
|
+
else None,
|
|
1380
|
+
"html_url": comment.html_url,
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
def update_issue_comment(
|
|
1384
|
+
self,
|
|
1385
|
+
comment_id: int,
|
|
1386
|
+
body: str,
|
|
1387
|
+
owner: Optional[str] = None,
|
|
1388
|
+
repo: Optional[str] = None,
|
|
1389
|
+
) -> Dict[str, Any]:
|
|
1390
|
+
"""
|
|
1391
|
+
Update an existing comment.
|
|
1392
|
+
|
|
1393
|
+
Args:
|
|
1394
|
+
comment_id: Comment ID
|
|
1395
|
+
body: New comment body
|
|
1396
|
+
owner: Repository owner
|
|
1397
|
+
repo: Repository name
|
|
1398
|
+
|
|
1399
|
+
Returns:
|
|
1400
|
+
Updated comment dict
|
|
1401
|
+
"""
|
|
1402
|
+
gh_repo = self._get_repo(owner, repo)
|
|
1403
|
+
comment = gh_repo.get_issue_comment(comment_id)
|
|
1404
|
+
comment.edit(body)
|
|
1405
|
+
|
|
1406
|
+
return {
|
|
1407
|
+
"id": comment.id,
|
|
1408
|
+
"body": comment.body,
|
|
1409
|
+
"author": comment.user.login if comment.user else "unknown",
|
|
1410
|
+
"created_at": comment.created_at.isoformat(),
|
|
1411
|
+
"updated_at": comment.updated_at.isoformat()
|
|
1412
|
+
if comment.updated_at
|
|
1413
|
+
else None,
|
|
1414
|
+
"html_url": comment.html_url,
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
def delete_issue_comment(
|
|
1418
|
+
self,
|
|
1419
|
+
comment_id: int,
|
|
1420
|
+
owner: Optional[str] = None,
|
|
1421
|
+
repo: Optional[str] = None,
|
|
1422
|
+
) -> Dict[str, Any]:
|
|
1423
|
+
"""
|
|
1424
|
+
Delete a comment.
|
|
1425
|
+
|
|
1426
|
+
Args:
|
|
1427
|
+
comment_id: Comment ID
|
|
1428
|
+
owner: Repository owner
|
|
1429
|
+
repo: Repository name
|
|
1430
|
+
|
|
1431
|
+
Returns:
|
|
1432
|
+
Dict with deleted comment_id
|
|
1433
|
+
"""
|
|
1434
|
+
gh_repo = self._get_repo(owner, repo)
|
|
1435
|
+
comment = gh_repo.get_issue_comment(comment_id)
|
|
1436
|
+
comment.delete()
|
|
1437
|
+
|
|
1438
|
+
return {
|
|
1439
|
+
"deleted": True,
|
|
1440
|
+
"comment_id": comment_id,
|
|
1441
|
+
}
|
|
1442
|
+
|
|
761
1443
|
def get_issue(
|
|
762
1444
|
self,
|
|
763
1445
|
issue_number: int,
|
mcp_server/tools/github_tools.py
CHANGED
|
@@ -571,11 +571,12 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
571
571
|
action: str = Field(
|
|
572
572
|
...,
|
|
573
573
|
description="Action: 'list', 'view', 'create', 'update', 'close', 'reopen', 'comment', "
|
|
574
|
+
"'list_comments', 'update_comment', 'delete_comment', "
|
|
574
575
|
"'add_sub_issue', 'remove_sub_issue', 'list_sub_issues'",
|
|
575
576
|
),
|
|
576
577
|
issue_numbers: Optional[List[int]] = Field(
|
|
577
578
|
default=None,
|
|
578
|
-
description="Issue number(s). Required for view/update/close/reopen/comment/sub-issue/
|
|
579
|
+
description="Issue number(s). Required for view/update/close/reopen/comment/sub-issue/comment ops.",
|
|
579
580
|
),
|
|
580
581
|
title: Optional[str] = Field(
|
|
581
582
|
default=None,
|
|
@@ -583,7 +584,7 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
583
584
|
),
|
|
584
585
|
body: Optional[str] = Field(
|
|
585
586
|
default=None,
|
|
586
|
-
description="Issue body (for 'create'/'update') or comment text (for 'comment')",
|
|
587
|
+
description="Issue body (for 'create'/'update') or comment text (for 'comment'/'update_comment')",
|
|
587
588
|
),
|
|
588
589
|
labels: Optional[List[str]] = Field(
|
|
589
590
|
default=None,
|
|
@@ -602,6 +603,18 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
602
603
|
description="Parent issue number. For 'create': attach new issue as sub-issue. "
|
|
603
604
|
"For 'add_sub_issue'/'remove_sub_issue'/'list_sub_issues': the parent issue.",
|
|
604
605
|
),
|
|
606
|
+
comment_id: Optional[int] = Field(
|
|
607
|
+
default=None,
|
|
608
|
+
description="Comment ID for 'update_comment' or 'delete_comment' actions.",
|
|
609
|
+
),
|
|
610
|
+
comments_limit: Optional[int] = Field(
|
|
611
|
+
default=10,
|
|
612
|
+
description="Max comments to return for 'list_comments' (default: 10).",
|
|
613
|
+
),
|
|
614
|
+
comments_order: Optional[str] = Field(
|
|
615
|
+
default="asc",
|
|
616
|
+
description="Comment order for 'list_comments': 'asc' (oldest first) or 'desc' (newest first).",
|
|
617
|
+
),
|
|
605
618
|
owner: Optional[str] = Field(
|
|
606
619
|
default=None,
|
|
607
620
|
description="Repository owner",
|
|
@@ -644,6 +657,10 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
644
657
|
- create as sub-issue: manage_issues(action="create", title="Task 1", parent_issue=42)
|
|
645
658
|
- close multiple: manage_issues(action="close", issue_numbers=[1, 2, 3])
|
|
646
659
|
- comment: manage_issues(action="comment", issue_numbers=[42], body="Fixed!")
|
|
660
|
+
- list comments: manage_issues(action="list_comments", issue_numbers=[42], comments_limit=5)
|
|
661
|
+
- list newest comments: manage_issues(action="list_comments", issue_numbers=[42], comments_order="desc")
|
|
662
|
+
- update comment: manage_issues(action="update_comment", issue_numbers=[42], comment_id=123, body="Updated")
|
|
663
|
+
- delete comment: manage_issues(action="delete_comment", issue_numbers=[42], comment_id=123)
|
|
647
664
|
- add sub-issues: manage_issues(action="add_sub_issue", issue_numbers=[43,44], parent_issue=42)
|
|
648
665
|
- remove sub-issue: manage_issues(action="remove_sub_issue", issue_numbers=[43], parent_issue=42)
|
|
649
666
|
- list sub-issues: manage_issues(action="list_sub_issues", parent_issue=42)
|
|
@@ -830,6 +847,66 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
830
847
|
}
|
|
831
848
|
)
|
|
832
849
|
|
|
850
|
+
# === LIST COMMENTS ACTION ===
|
|
851
|
+
elif action == "list_comments":
|
|
852
|
+
comments = client.list_issue_comments(
|
|
853
|
+
issue_number=issue_number,
|
|
854
|
+
owner=owner,
|
|
855
|
+
repo=repo,
|
|
856
|
+
limit=comments_limit or 10,
|
|
857
|
+
order=comments_order or "asc",
|
|
858
|
+
)
|
|
859
|
+
results.append(
|
|
860
|
+
{
|
|
861
|
+
"number": issue_number,
|
|
862
|
+
"comments_count": len(comments),
|
|
863
|
+
"comments": comments,
|
|
864
|
+
}
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
# === UPDATE COMMENT ACTION ===
|
|
868
|
+
elif action == "update_comment":
|
|
869
|
+
if not comment_id:
|
|
870
|
+
raise ToolError(
|
|
871
|
+
"'comment_id' is required for 'update_comment' action"
|
|
872
|
+
)
|
|
873
|
+
if not body:
|
|
874
|
+
raise ToolError(
|
|
875
|
+
"'body' is required for 'update_comment' action"
|
|
876
|
+
)
|
|
877
|
+
updated = client.update_issue_comment(
|
|
878
|
+
comment_id=comment_id,
|
|
879
|
+
body=body,
|
|
880
|
+
owner=owner,
|
|
881
|
+
repo=repo,
|
|
882
|
+
)
|
|
883
|
+
results.append(
|
|
884
|
+
{
|
|
885
|
+
"number": issue_number,
|
|
886
|
+
"status": "comment_updated",
|
|
887
|
+
"comment": updated,
|
|
888
|
+
}
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
# === DELETE COMMENT ACTION ===
|
|
892
|
+
elif action == "delete_comment":
|
|
893
|
+
if not comment_id:
|
|
894
|
+
raise ToolError(
|
|
895
|
+
"'comment_id' is required for 'delete_comment' action"
|
|
896
|
+
)
|
|
897
|
+
client.delete_issue_comment(
|
|
898
|
+
comment_id=comment_id,
|
|
899
|
+
owner=owner,
|
|
900
|
+
repo=repo,
|
|
901
|
+
)
|
|
902
|
+
results.append(
|
|
903
|
+
{
|
|
904
|
+
"number": issue_number,
|
|
905
|
+
"status": "comment_deleted",
|
|
906
|
+
"comment_id": comment_id,
|
|
907
|
+
}
|
|
908
|
+
)
|
|
909
|
+
|
|
833
910
|
else:
|
|
834
911
|
raise ToolError(f"Invalid action: {action}")
|
|
835
912
|
|
|
@@ -841,6 +918,13 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
841
918
|
"issues": results,
|
|
842
919
|
}
|
|
843
920
|
|
|
921
|
+
if action == "list_comments":
|
|
922
|
+
return {
|
|
923
|
+
"action": "list_comments",
|
|
924
|
+
"count": len(results),
|
|
925
|
+
"issues": results,
|
|
926
|
+
}
|
|
927
|
+
|
|
844
928
|
return {"action": action, "count": len(results), "results": results}
|
|
845
929
|
|
|
846
930
|
except ToolError:
|
|
@@ -850,6 +934,404 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
850
934
|
except Exception as e:
|
|
851
935
|
raise ToolError(f"Failed to {action} issue(s): {str(e)}")
|
|
852
936
|
|
|
937
|
+
@mcp.tool(tags={"github", "prs"})
|
|
938
|
+
def manage_prs(
|
|
939
|
+
action: str = Field(
|
|
940
|
+
...,
|
|
941
|
+
description="Action: 'list', 'view', 'create', 'update', 'merge', 'close', 'reopen', "
|
|
942
|
+
"'comment', 'request_reviewers', 'review', 'to_draft', 'ready_for_review', "
|
|
943
|
+
"'add_labels', 'remove_labels', 'add_assignees', 'remove_assignees'",
|
|
944
|
+
),
|
|
945
|
+
pr_numbers: Optional[List[int]] = Field(
|
|
946
|
+
default=None,
|
|
947
|
+
description="PR number(s). Required for view/update/merge/close/reopen/comment/review/labels/assignees actions.",
|
|
948
|
+
),
|
|
949
|
+
title: Optional[str] = Field(
|
|
950
|
+
default=None,
|
|
951
|
+
description="PR title (for 'create' or 'update')",
|
|
952
|
+
),
|
|
953
|
+
body: Optional[str] = Field(
|
|
954
|
+
default=None,
|
|
955
|
+
description="PR body/description (for 'create'/'update') or comment text (for 'comment')",
|
|
956
|
+
),
|
|
957
|
+
head: Optional[str] = Field(
|
|
958
|
+
default=None,
|
|
959
|
+
description="Source branch containing changes (for 'create')",
|
|
960
|
+
),
|
|
961
|
+
base: Optional[str] = Field(
|
|
962
|
+
default=None,
|
|
963
|
+
description="Target branch to merge into (for 'create', default: repo default branch)",
|
|
964
|
+
),
|
|
965
|
+
draft: Optional[bool] = Field(
|
|
966
|
+
default=None,
|
|
967
|
+
description="Create as draft PR (for 'create')",
|
|
968
|
+
),
|
|
969
|
+
merge_method: Optional[str] = Field(
|
|
970
|
+
default="merge",
|
|
971
|
+
description="Merge strategy: 'merge', 'squash', or 'rebase' (for 'merge' action)",
|
|
972
|
+
),
|
|
973
|
+
reviewers: Optional[List[str]] = Field(
|
|
974
|
+
default=None,
|
|
975
|
+
description="GitHub usernames to request review from (for 'request_reviewers')",
|
|
976
|
+
),
|
|
977
|
+
team_reviewers: Optional[List[str]] = Field(
|
|
978
|
+
default=None,
|
|
979
|
+
description="Team slugs to request review from (for 'request_reviewers')",
|
|
980
|
+
),
|
|
981
|
+
review_event: Optional[str] = Field(
|
|
982
|
+
default=None,
|
|
983
|
+
description="Review event: 'APPROVE', 'REQUEST_CHANGES', or 'COMMENT' (for 'review' action)",
|
|
984
|
+
),
|
|
985
|
+
labels: Optional[List[str]] = Field(
|
|
986
|
+
default=None,
|
|
987
|
+
description="Labels (for 'add_labels' or 'remove_labels')",
|
|
988
|
+
),
|
|
989
|
+
assignees: Optional[List[str]] = Field(
|
|
990
|
+
default=None,
|
|
991
|
+
description="GitHub usernames for assignees. For 'create': defaults to self if not specified. "
|
|
992
|
+
"For 'add_assignees'/'remove_assignees': required.",
|
|
993
|
+
),
|
|
994
|
+
owner: Optional[str] = Field(
|
|
995
|
+
default=None,
|
|
996
|
+
description="Repository owner",
|
|
997
|
+
),
|
|
998
|
+
repo: Optional[str] = Field(
|
|
999
|
+
default=None,
|
|
1000
|
+
description="Repository name. Required.",
|
|
1001
|
+
),
|
|
1002
|
+
state: Optional[str] = Field(
|
|
1003
|
+
default="open",
|
|
1004
|
+
description="PR state filter for 'list': 'open', 'closed', or 'all' (default: 'open')",
|
|
1005
|
+
),
|
|
1006
|
+
limit: Optional[int] = Field(
|
|
1007
|
+
default=20,
|
|
1008
|
+
description="Maximum PRs to return for 'list' action (default: 20)",
|
|
1009
|
+
),
|
|
1010
|
+
) -> dict:
|
|
1011
|
+
"""
|
|
1012
|
+
Manage GitHub pull requests: list, view, create, update, merge, close, reopen, comment, and review.
|
|
1013
|
+
|
|
1014
|
+
Supports bulk operations for view/close/reopen/comment via pr_numbers list.
|
|
1015
|
+
|
|
1016
|
+
Examples:
|
|
1017
|
+
- list: manage_prs(action="list", state="open")
|
|
1018
|
+
- view: manage_prs(action="view", pr_numbers=[42])
|
|
1019
|
+
- create: manage_prs(action="create", title="Feature X", head="feature-branch", base="main")
|
|
1020
|
+
- create draft: manage_prs(action="create", title="WIP", head="wip-branch", draft=True)
|
|
1021
|
+
- update: manage_prs(action="update", pr_numbers=[42], title="New title", body="Updated desc")
|
|
1022
|
+
- merge: manage_prs(action="merge", pr_numbers=[42], merge_method="squash")
|
|
1023
|
+
- close: manage_prs(action="close", pr_numbers=[42])
|
|
1024
|
+
- reopen: manage_prs(action="reopen", pr_numbers=[42])
|
|
1025
|
+
- comment: manage_prs(action="comment", pr_numbers=[42], body="LGTM!")
|
|
1026
|
+
- request review: manage_prs(action="request_reviewers", pr_numbers=[42], reviewers=["user1"])
|
|
1027
|
+
- approve: manage_prs(action="review", pr_numbers=[42], review_event="APPROVE", body="Looks good!")
|
|
1028
|
+
- to draft: manage_prs(action="to_draft", pr_numbers=[42])
|
|
1029
|
+
- ready: manage_prs(action="ready_for_review", pr_numbers=[42])
|
|
1030
|
+
- add labels: manage_prs(action="add_labels", pr_numbers=[42], labels=["bug", "urgent"])
|
|
1031
|
+
- remove labels: manage_prs(action="remove_labels", pr_numbers=[42], labels=["wip"])
|
|
1032
|
+
- add assignees: manage_prs(action="add_assignees", pr_numbers=[42], assignees=["user1"])
|
|
1033
|
+
- remove assignees: manage_prs(action="remove_assignees", pr_numbers=[42], assignees=["user1"])
|
|
1034
|
+
"""
|
|
1035
|
+
try:
|
|
1036
|
+
client = _get_client()
|
|
1037
|
+
|
|
1038
|
+
# === LIST ACTION ===
|
|
1039
|
+
if action == "list":
|
|
1040
|
+
prs = client.list_prs(
|
|
1041
|
+
owner=owner,
|
|
1042
|
+
repo=repo,
|
|
1043
|
+
state=state or "open",
|
|
1044
|
+
limit=limit or 20,
|
|
1045
|
+
detail_level="summary",
|
|
1046
|
+
)
|
|
1047
|
+
return {
|
|
1048
|
+
"action": "list",
|
|
1049
|
+
"state": state or "open",
|
|
1050
|
+
"count": len(prs),
|
|
1051
|
+
"prs": [pr.model_dump() for pr in prs],
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
# === CREATE ACTION ===
|
|
1055
|
+
if action == "create":
|
|
1056
|
+
if not title:
|
|
1057
|
+
raise ToolError("'title' is required for 'create' action")
|
|
1058
|
+
if not head:
|
|
1059
|
+
raise ToolError(
|
|
1060
|
+
"'head' (source branch) is required for 'create' action"
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
pr = client.create_pr(
|
|
1064
|
+
title=title,
|
|
1065
|
+
head=head,
|
|
1066
|
+
base=base or "main",
|
|
1067
|
+
body=body,
|
|
1068
|
+
draft=draft or False,
|
|
1069
|
+
owner=owner,
|
|
1070
|
+
repo=repo,
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
result = {
|
|
1074
|
+
"action": "created",
|
|
1075
|
+
"pr": pr.model_dump(),
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
# Auto-assign to self if assignees not specified
|
|
1079
|
+
pr_assignees = assignees
|
|
1080
|
+
if pr_assignees is None:
|
|
1081
|
+
# Default to current user
|
|
1082
|
+
current_user = client.get_authenticated_user()
|
|
1083
|
+
if current_user:
|
|
1084
|
+
pr_assignees = [current_user]
|
|
1085
|
+
|
|
1086
|
+
if pr_assignees:
|
|
1087
|
+
try:
|
|
1088
|
+
assign_result = client.add_pr_assignees(
|
|
1089
|
+
pr.number, assignees=pr_assignees, owner=owner, repo=repo
|
|
1090
|
+
)
|
|
1091
|
+
result["assignees"] = assign_result["assignees"]
|
|
1092
|
+
except Exception as e:
|
|
1093
|
+
result["assignee_error"] = str(e)
|
|
1094
|
+
|
|
1095
|
+
return result
|
|
1096
|
+
|
|
1097
|
+
# === ALL OTHER ACTIONS REQUIRE pr_numbers ===
|
|
1098
|
+
if not pr_numbers:
|
|
1099
|
+
raise ToolError(f"'pr_numbers' required for '{action}' action")
|
|
1100
|
+
|
|
1101
|
+
results = []
|
|
1102
|
+
for pr_number in pr_numbers:
|
|
1103
|
+
# === VIEW ACTION ===
|
|
1104
|
+
if action == "view":
|
|
1105
|
+
pr_data = client.get_pr(
|
|
1106
|
+
pr_number=pr_number,
|
|
1107
|
+
owner=owner,
|
|
1108
|
+
repo=repo,
|
|
1109
|
+
)
|
|
1110
|
+
if pr_data:
|
|
1111
|
+
results.append(pr_data.model_dump())
|
|
1112
|
+
else:
|
|
1113
|
+
results.append({"number": pr_number, "error": "PR not found"})
|
|
1114
|
+
|
|
1115
|
+
# === UPDATE ACTION ===
|
|
1116
|
+
elif action == "update":
|
|
1117
|
+
pr_data = client.update_pr(
|
|
1118
|
+
pr_number=pr_number,
|
|
1119
|
+
title=title,
|
|
1120
|
+
body=body,
|
|
1121
|
+
base=base,
|
|
1122
|
+
owner=owner,
|
|
1123
|
+
repo=repo,
|
|
1124
|
+
)
|
|
1125
|
+
results.append(
|
|
1126
|
+
{
|
|
1127
|
+
"number": pr_number,
|
|
1128
|
+
"status": "updated",
|
|
1129
|
+
"pr": pr_data.model_dump(),
|
|
1130
|
+
}
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
# === MERGE ACTION ===
|
|
1134
|
+
elif action == "merge":
|
|
1135
|
+
merge_result = client.merge_pr(
|
|
1136
|
+
pr_number=pr_number,
|
|
1137
|
+
merge_method=merge_method or "merge",
|
|
1138
|
+
owner=owner,
|
|
1139
|
+
repo=repo,
|
|
1140
|
+
)
|
|
1141
|
+
results.append(
|
|
1142
|
+
{
|
|
1143
|
+
"number": pr_number,
|
|
1144
|
+
"status": "merged" if merge_result["merged"] else "failed",
|
|
1145
|
+
"message": merge_result["message"],
|
|
1146
|
+
"sha": merge_result["sha"],
|
|
1147
|
+
}
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
# === CLOSE ACTION ===
|
|
1151
|
+
elif action == "close":
|
|
1152
|
+
client.close_pr(pr_number, owner=owner, repo=repo)
|
|
1153
|
+
results.append({"number": pr_number, "status": "closed"})
|
|
1154
|
+
|
|
1155
|
+
# === REOPEN ACTION ===
|
|
1156
|
+
elif action == "reopen":
|
|
1157
|
+
client.reopen_pr(pr_number, owner=owner, repo=repo)
|
|
1158
|
+
results.append({"number": pr_number, "status": "reopened"})
|
|
1159
|
+
|
|
1160
|
+
# === COMMENT ACTION ===
|
|
1161
|
+
elif action == "comment":
|
|
1162
|
+
if not body:
|
|
1163
|
+
raise ToolError("'body' is required for 'comment' action")
|
|
1164
|
+
comment = client.add_pr_comment(
|
|
1165
|
+
pr_number, body=body, owner=owner, repo=repo
|
|
1166
|
+
)
|
|
1167
|
+
results.append(
|
|
1168
|
+
{
|
|
1169
|
+
"number": pr_number,
|
|
1170
|
+
"status": "commented",
|
|
1171
|
+
"comment_url": comment["html_url"],
|
|
1172
|
+
}
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
# === REQUEST REVIEWERS ACTION ===
|
|
1176
|
+
elif action == "request_reviewers":
|
|
1177
|
+
if not reviewers and not team_reviewers:
|
|
1178
|
+
raise ToolError(
|
|
1179
|
+
"'reviewers' or 'team_reviewers' required for 'request_reviewers' action"
|
|
1180
|
+
)
|
|
1181
|
+
review_result = client.request_reviewers(
|
|
1182
|
+
pr_number,
|
|
1183
|
+
reviewers=reviewers,
|
|
1184
|
+
team_reviewers=team_reviewers,
|
|
1185
|
+
owner=owner,
|
|
1186
|
+
repo=repo,
|
|
1187
|
+
)
|
|
1188
|
+
results.append(
|
|
1189
|
+
{
|
|
1190
|
+
"number": pr_number,
|
|
1191
|
+
"status": "reviewers_requested",
|
|
1192
|
+
"requested_reviewers": review_result["requested_reviewers"],
|
|
1193
|
+
"requested_teams": review_result["requested_teams"],
|
|
1194
|
+
}
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
# === REVIEW ACTION ===
|
|
1198
|
+
elif action == "review":
|
|
1199
|
+
if not review_event:
|
|
1200
|
+
raise ToolError(
|
|
1201
|
+
"'review_event' (APPROVE, REQUEST_CHANGES, COMMENT) required for 'review' action"
|
|
1202
|
+
)
|
|
1203
|
+
if review_event == "REQUEST_CHANGES" and not body:
|
|
1204
|
+
raise ToolError("'body' is required when requesting changes")
|
|
1205
|
+
review_result = client.submit_pr_review(
|
|
1206
|
+
pr_number,
|
|
1207
|
+
event=review_event,
|
|
1208
|
+
body=body,
|
|
1209
|
+
owner=owner,
|
|
1210
|
+
repo=repo,
|
|
1211
|
+
)
|
|
1212
|
+
results.append(
|
|
1213
|
+
{
|
|
1214
|
+
"number": pr_number,
|
|
1215
|
+
"status": "reviewed",
|
|
1216
|
+
"review_state": review_result["state"],
|
|
1217
|
+
"review_url": review_result["html_url"],
|
|
1218
|
+
}
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
# === TO DRAFT ACTION ===
|
|
1222
|
+
elif action == "to_draft":
|
|
1223
|
+
draft_result = client.convert_pr_to_draft(
|
|
1224
|
+
pr_number, owner=owner, repo=repo
|
|
1225
|
+
)
|
|
1226
|
+
results.append(
|
|
1227
|
+
{
|
|
1228
|
+
"number": pr_number,
|
|
1229
|
+
"status": "converted_to_draft",
|
|
1230
|
+
"is_draft": draft_result["is_draft"],
|
|
1231
|
+
"message": draft_result["message"],
|
|
1232
|
+
}
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
# === READY FOR REVIEW ACTION ===
|
|
1236
|
+
elif action == "ready_for_review":
|
|
1237
|
+
ready_result = client.mark_pr_ready_for_review(
|
|
1238
|
+
pr_number, owner=owner, repo=repo
|
|
1239
|
+
)
|
|
1240
|
+
results.append(
|
|
1241
|
+
{
|
|
1242
|
+
"number": pr_number,
|
|
1243
|
+
"status": "marked_ready",
|
|
1244
|
+
"is_draft": ready_result["is_draft"],
|
|
1245
|
+
"message": ready_result["message"],
|
|
1246
|
+
}
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
# === ADD LABELS ACTION ===
|
|
1250
|
+
elif action == "add_labels":
|
|
1251
|
+
if not labels:
|
|
1252
|
+
raise ToolError("'labels' is required for 'add_labels' action")
|
|
1253
|
+
label_result = client.add_pr_labels(
|
|
1254
|
+
pr_number, labels=labels, owner=owner, repo=repo
|
|
1255
|
+
)
|
|
1256
|
+
results.append(
|
|
1257
|
+
{
|
|
1258
|
+
"number": pr_number,
|
|
1259
|
+
"status": "labels_added",
|
|
1260
|
+
"labels": label_result["labels"],
|
|
1261
|
+
}
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
# === REMOVE LABELS ACTION ===
|
|
1265
|
+
elif action == "remove_labels":
|
|
1266
|
+
if not labels:
|
|
1267
|
+
raise ToolError(
|
|
1268
|
+
"'labels' is required for 'remove_labels' action"
|
|
1269
|
+
)
|
|
1270
|
+
label_result = client.remove_pr_labels(
|
|
1271
|
+
pr_number, labels=labels, owner=owner, repo=repo
|
|
1272
|
+
)
|
|
1273
|
+
results.append(
|
|
1274
|
+
{
|
|
1275
|
+
"number": pr_number,
|
|
1276
|
+
"status": "labels_removed",
|
|
1277
|
+
"labels": label_result["labels"],
|
|
1278
|
+
}
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
# === ADD ASSIGNEES ACTION ===
|
|
1282
|
+
elif action == "add_assignees":
|
|
1283
|
+
if not assignees:
|
|
1284
|
+
raise ToolError(
|
|
1285
|
+
"'assignees' is required for 'add_assignees' action"
|
|
1286
|
+
)
|
|
1287
|
+
assign_result = client.add_pr_assignees(
|
|
1288
|
+
pr_number, assignees=assignees, owner=owner, repo=repo
|
|
1289
|
+
)
|
|
1290
|
+
results.append(
|
|
1291
|
+
{
|
|
1292
|
+
"number": pr_number,
|
|
1293
|
+
"status": "assignees_added",
|
|
1294
|
+
"assignees": assign_result["assignees"],
|
|
1295
|
+
}
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
# === REMOVE ASSIGNEES ACTION ===
|
|
1299
|
+
elif action == "remove_assignees":
|
|
1300
|
+
if not assignees:
|
|
1301
|
+
raise ToolError(
|
|
1302
|
+
"'assignees' is required for 'remove_assignees' action"
|
|
1303
|
+
)
|
|
1304
|
+
assign_result = client.remove_pr_assignees(
|
|
1305
|
+
pr_number, assignees=assignees, owner=owner, repo=repo
|
|
1306
|
+
)
|
|
1307
|
+
results.append(
|
|
1308
|
+
{
|
|
1309
|
+
"number": pr_number,
|
|
1310
|
+
"status": "assignees_removed",
|
|
1311
|
+
"assignees": assign_result["assignees"],
|
|
1312
|
+
}
|
|
1313
|
+
)
|
|
1314
|
+
|
|
1315
|
+
else:
|
|
1316
|
+
raise ToolError(f"Invalid action: {action}")
|
|
1317
|
+
|
|
1318
|
+
# Return format depends on action
|
|
1319
|
+
if action == "view":
|
|
1320
|
+
return {
|
|
1321
|
+
"action": "view",
|
|
1322
|
+
"count": len(results),
|
|
1323
|
+
"prs": results,
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
return {"action": action, "count": len(results), "results": results}
|
|
1327
|
+
|
|
1328
|
+
except ToolError:
|
|
1329
|
+
raise
|
|
1330
|
+
except ValueError as e:
|
|
1331
|
+
raise ToolError(f"Repository not specified: {str(e)}")
|
|
1332
|
+
except Exception as e:
|
|
1333
|
+
raise ToolError(f"Failed to {action} PR(s): {str(e)}")
|
|
1334
|
+
|
|
853
1335
|
@mcp.tool(tags={"github", "prs", "appraisal"})
|
|
854
1336
|
def prepare_appraisal_data(
|
|
855
1337
|
author: Optional[str] = Field(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quickcall-integrations
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: MCP server with developer integrations for Claude Code and Cursor
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: fastmcp>=2.13.0
|
|
@@ -90,7 +90,7 @@ Add to MCP config (`~/.cursor/mcp.json` or `.cursor/mcp.json`):
|
|
|
90
90
|
| **Slack** | Read/send messages, threads, channels | Yes |
|
|
91
91
|
|
|
92
92
|
<details>
|
|
93
|
-
<summary><strong>Available Tools (
|
|
93
|
+
<summary><strong>Available Tools (26)</strong></summary>
|
|
94
94
|
|
|
95
95
|
### Git
|
|
96
96
|
| Tool | Description |
|
|
@@ -107,6 +107,8 @@ Add to MCP config (`~/.cursor/mcp.json` or `.cursor/mcp.json`):
|
|
|
107
107
|
| `get_commit` | Get commit details (message, stats, files) |
|
|
108
108
|
| `list_branches` | List repository branches |
|
|
109
109
|
| `manage_issues` | List, view, create, update, close, reopen, comment on issues + sub-issues |
|
|
110
|
+
| `manage_prs` | Full PR lifecycle: create, update, merge, close, review, comment, labels, assignees |
|
|
111
|
+
| `manage_projects` | GitHub Projects V2: add issues, update fields (Status, Priority, etc.) |
|
|
110
112
|
| `check_github_connection` | Verify GitHub connection |
|
|
111
113
|
|
|
112
114
|
### Slack
|
|
@@ -193,6 +195,8 @@ GITHUB_USERNAME=your-username # Optional: for better UX
|
|
|
193
195
|
| `/quickcall:status` | Show connection status |
|
|
194
196
|
| `/quickcall:updates` | Get git updates (default: 1 day) |
|
|
195
197
|
| `/quickcall:updates 7d` | Get updates for last 7 days |
|
|
198
|
+
| `/quickcall:pr-summary` | List open PRs for a repo |
|
|
199
|
+
| `/quickcall:pr-summary owner/repo` | List PRs for specific repo |
|
|
196
200
|
| `/quickcall:slack-summary` | Summarize Slack messages (default: 1 day) |
|
|
197
201
|
| `/quickcall:slack-summary 7d` | Summarize last 7 days |
|
|
198
202
|
|
|
@@ -248,10 +252,28 @@ The `manage_issues` tool provides full issue lifecycle management:
|
|
|
248
252
|
| `close` | Close issue(s) |
|
|
249
253
|
| `reopen` | Reopen issue(s) |
|
|
250
254
|
| `comment` | Add comment to issue(s) |
|
|
255
|
+
| `list_comments` | List comments with limit and order |
|
|
256
|
+
| `update_comment` | Edit existing comment by ID |
|
|
257
|
+
| `delete_comment` | Delete comment by ID |
|
|
251
258
|
| `add_sub_issue` | Add child issue to parent |
|
|
252
259
|
| `remove_sub_issue` | Remove child from parent |
|
|
253
260
|
| `list_sub_issues` | List sub-issues of a parent |
|
|
254
261
|
|
|
262
|
+
### Comment Management
|
|
263
|
+
|
|
264
|
+
| Parameter | Description |
|
|
265
|
+
|-----------|-------------|
|
|
266
|
+
| `comment_id` | Comment ID for update/delete operations |
|
|
267
|
+
| `comments_limit` | Max comments to return (default: 10) |
|
|
268
|
+
| `comments_order` | `'asc'` (oldest first) or `'desc'` (newest first) |
|
|
269
|
+
|
|
270
|
+
**Examples:**
|
|
271
|
+
```
|
|
272
|
+
List last 5 comments on issue #42 (newest first)
|
|
273
|
+
Update comment 123456 on issue #42 with new text
|
|
274
|
+
Delete comment 123456 from issue #42
|
|
275
|
+
```
|
|
276
|
+
|
|
255
277
|
### List Filters
|
|
256
278
|
|
|
257
279
|
| Filter | Description |
|
|
@@ -327,6 +349,46 @@ Create a bug report issue titled "Login fails on Safari"
|
|
|
327
349
|
Create issue with feature_request template
|
|
328
350
|
```
|
|
329
351
|
|
|
352
|
+
## PR Management
|
|
353
|
+
|
|
354
|
+
The `manage_prs` tool provides full pull request lifecycle management:
|
|
355
|
+
|
|
356
|
+
### Actions
|
|
357
|
+
|
|
358
|
+
| Action | Description |
|
|
359
|
+
|--------|-------------|
|
|
360
|
+
| `list` | List PRs with state filter |
|
|
361
|
+
| `view` | View PR details |
|
|
362
|
+
| `create` | Create PR (auto-assigns to self) |
|
|
363
|
+
| `update` | Update title, body, base branch |
|
|
364
|
+
| `merge` | Merge PR (merge/squash/rebase) |
|
|
365
|
+
| `close` | Close PR without merging |
|
|
366
|
+
| `reopen` | Reopen closed PR |
|
|
367
|
+
| `comment` | Add comment to PR |
|
|
368
|
+
| `request_reviewers` | Request user/team reviewers |
|
|
369
|
+
| `review` | Submit review (APPROVE, REQUEST_CHANGES, COMMENT) |
|
|
370
|
+
| `to_draft` | Convert PR to draft |
|
|
371
|
+
| `ready_for_review` | Mark draft as ready |
|
|
372
|
+
| `add_labels` | Add labels to PR |
|
|
373
|
+
| `remove_labels` | Remove labels from PR |
|
|
374
|
+
| `add_assignees` | Add assignees |
|
|
375
|
+
| `remove_assignees` | Remove assignees |
|
|
376
|
+
|
|
377
|
+
### Examples
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
List open PRs on my-repo
|
|
381
|
+
Create a PR from feature-branch to main titled "Add new feature"
|
|
382
|
+
Merge PR #42 with squash
|
|
383
|
+
Request review from @alice on PR #42
|
|
384
|
+
Approve PR #42 with comment "LGTM!"
|
|
385
|
+
Add labels bug and urgent to PR #42
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Auto-assign
|
|
389
|
+
|
|
390
|
+
When creating a PR, it automatically assigns to the current user unless `assignees` is explicitly provided.
|
|
391
|
+
|
|
330
392
|
## Troubleshooting
|
|
331
393
|
|
|
332
394
|
### Clean Reinstall
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
mcp_server/__init__.py,sha256=6KGzjSPyVB6vQh150DwBjINM_CsZNDhOzwSQFWpXz0U,301
|
|
2
2
|
mcp_server/server.py,sha256=kv5hh0J-M7yENUBBNI1bkq1y7MB0zn5R_-R1tib6_sk,3108
|
|
3
3
|
mcp_server/api_clients/__init__.py,sha256=kOG5_sxIVpAx_tvf1nq_P0QCkqojAVidRE-wenLS-Wc,207
|
|
4
|
-
mcp_server/api_clients/github_client.py,sha256=
|
|
4
|
+
mcp_server/api_clients/github_client.py,sha256=oKB7NyjuCvs9-BI2061XRxfxIM6lMrrBJJfbCgUxizE,90249
|
|
5
5
|
mcp_server/api_clients/slack_client.py,sha256=w3rcGghttfYw8Ird2beNo2LEYLc3rCTbUKMH4X7QQuQ,16447
|
|
6
6
|
mcp_server/auth/__init__.py,sha256=D-JS0Qe7FkeJjYx92u_AqPx8ZRoB3dKMowzzJXlX6cc,780
|
|
7
7
|
mcp_server/auth/credentials.py,sha256=sDS0W5c16i_UGvhG8Sh1RO93FxRn-hHVAdI9hlWuhx0,20011
|
|
@@ -12,10 +12,10 @@ mcp_server/resources/slack_resources.py,sha256=b_CPxAicwkF3PsBXIat4QoLbDUHM2g_iP
|
|
|
12
12
|
mcp_server/tools/__init__.py,sha256=vIR2ujAaTXm2DgpTsVNz3brI4G34p-Jeg44Qe0uvWc0,405
|
|
13
13
|
mcp_server/tools/auth_tools.py,sha256=BPuj9M0pZOvvWHxH0HPdiVm-Y6DJyD-PEvtrIh68vbc,25409
|
|
14
14
|
mcp_server/tools/git_tools.py,sha256=jyCTQR2eSzUFXMt0Y8x66758-VY8YCY14DDUJt7GY2U,13957
|
|
15
|
-
mcp_server/tools/github_tools.py,sha256=
|
|
15
|
+
mcp_server/tools/github_tools.py,sha256=scfgo48_-uvuj5ehWaoB-VtKx5RqSME8fetpMphVh-w,70461
|
|
16
16
|
mcp_server/tools/slack_tools.py,sha256=-HVE_x3Z1KMeYGi1xhyppEwz5ZF-I-ZD0-Up8yBeoYE,11796
|
|
17
17
|
mcp_server/tools/utility_tools.py,sha256=oxAXpdqtPeB5Ug5dvk54V504r-8v1AO4_px-sO6LFOw,3910
|
|
18
|
-
quickcall_integrations-0.
|
|
19
|
-
quickcall_integrations-0.
|
|
20
|
-
quickcall_integrations-0.
|
|
21
|
-
quickcall_integrations-0.
|
|
18
|
+
quickcall_integrations-0.6.0.dist-info/METADATA,sha256=IG765WJlLD7A98ikmPH2Ru8fRQhcMFbanKS8eeQn-v4,11519
|
|
19
|
+
quickcall_integrations-0.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
20
|
+
quickcall_integrations-0.6.0.dist-info/entry_points.txt,sha256=kkcunmJUzncYvQ1rOR35V2LPm2HcFTKzdI2l3n7NwiM,66
|
|
21
|
+
quickcall_integrations-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
{quickcall_integrations-0.4.0.dist-info → quickcall_integrations-0.6.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|