@@ -5,13 +5,15 @@ import (
5
5
"encoding/json"
6
6
"fmt"
7
7
"io"
8
+ "math"
8
9
"net/http"
9
10
"time"
10
11
11
12
"github.com/github/github-mcp-server/pkg/translations"
12
13
"github.com/google/go-github/v69/github"
13
14
"github.com/mark3labs/mcp-go/mcp"
14
15
"github.com/mark3labs/mcp-go/server"
16
+ "github.com/shurcooL/githubv4"
15
17
)
16
18
17
19
// GetIssue creates a tool to get details of a specific issue in a GitHub repository.
@@ -711,6 +713,178 @@ func GetIssueComments(getClient GetClientFn, t translations.TranslationHelperFun
711
713
}
712
714
}
713
715
716
+ // AssignCopilotToIssue creates a tool to assign a Copilot to an issue.
717
+ // Note that this tool will not work on GHES where this feature is unsupported. In future, we should not expose this
718
+ // tool if the configured host does not support it.
719
+ func AssignCopilotToIssue (getGQLClient GetGQLClientFn , t translations.TranslationHelperFunc ) (mcp.Tool , server.ToolHandlerFunc ) {
720
+ return mcp .NewTool ("assign_copilot_to_issue" ,
721
+ mcp .WithDescription (t ("TOOL_ASSIGN_COPILOT_TO_ISSUE_DESCRIPTION" , "Assign a Copilot to a specific issue in a GitHub repository." )),
722
+ mcp .WithToolAnnotation (mcp.ToolAnnotation {
723
+ Title : t ("TOOL_ASSIGN_COPILOT_TO_ISSUE_USER_TITLE" , "Assign Copilot to issue" ),
724
+ ReadOnlyHint : toBoolPtr (false ),
725
+ }),
726
+ mcp .WithString ("owner" ,
727
+ mcp .Required (),
728
+ mcp .Description ("Repository owner" ),
729
+ ),
730
+ mcp .WithString ("repo" ,
731
+ mcp .Required (),
732
+ mcp .Description ("Repository name" ),
733
+ ),
734
+ mcp .WithNumber ("issueNumber" ,
735
+ mcp .Required (),
736
+ mcp .Description ("Issue number" ),
737
+ ),
738
+ ),
739
+ func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
740
+ owner , err := requiredParam [string ](request , "owner" )
741
+ if err != nil {
742
+ return mcp .NewToolResultError (err .Error ()), nil
743
+ }
744
+
745
+ repo , err := requiredParam [string ](request , "repo" )
746
+ if err != nil {
747
+ return mcp .NewToolResultError (err .Error ()), nil
748
+ }
749
+
750
+ issueNumber , err := RequiredInt (request , "issueNumber" )
751
+ if err != nil {
752
+ return mcp .NewToolResultError (err .Error ()), nil
753
+ }
754
+ if issueNumber < math .MinInt32 || issueNumber > math .MaxInt32 {
755
+ return mcp .NewToolResultError (fmt .Sprintf ("issueNumber %d overflows int32" , issueNumber )), nil
756
+ }
757
+
758
+ client , err := getGQLClient (ctx )
759
+ if err != nil {
760
+ return nil , fmt .Errorf ("failed to get GitHub client: %w" , err )
761
+ }
762
+
763
+ // First we need to get a list of assignable actors
764
+ type botAssignee struct {
765
+ ID githubv4.ID
766
+ Login string
767
+ TypeName string `graphql:"__typename"`
768
+ }
769
+
770
+ type suggestedActorsQuery struct {
771
+ Repository struct {
772
+ SuggestedActors struct {
773
+ Nodes []struct {
774
+ Bot botAssignee `graphql:"... on Bot"`
775
+ }
776
+ PageInfo struct {
777
+ HasNextPage bool
778
+ EndCursor string
779
+ }
780
+ } `graphql:"suggestedActors(first: 100, after: $endCursor, capabilities: CAN_BE_ASSIGNED)"`
781
+ } `graphql:"repository(owner: $owner, name: $name)"`
782
+ }
783
+
784
+ variables := map [string ]any {
785
+ "owner" : githubv4 .String (owner ),
786
+ "name" : githubv4 .String (repo ),
787
+ "endCursor" : (* githubv4 .String )(nil ),
788
+ }
789
+
790
+ var copilotAssignee * botAssignee
791
+ for {
792
+ var query suggestedActorsQuery
793
+ err := client .Query (ctx , & query , variables )
794
+ if err != nil {
795
+ return nil , err
796
+ }
797
+
798
+ for _ , node := range query .Repository .SuggestedActors .Nodes {
799
+ if node .Bot .Login == "copilot-swe-agent" {
800
+ copilotAssignee = & node .Bot
801
+ break
802
+ }
803
+ }
804
+
805
+ if ! query .Repository .SuggestedActors .PageInfo .HasNextPage {
806
+ break
807
+ }
808
+ variables ["endCursor" ] = githubv4 .String (query .Repository .SuggestedActors .PageInfo .EndCursor )
809
+ }
810
+
811
+ if copilotAssignee == nil {
812
+ return mcp .NewToolResultError ("Copilot was not found as a suggested assignee" ), nil
813
+ }
814
+
815
+ // Next let's get the GQL Node ID and current assignees for this issue
816
+ // because the only way to assign copilot is to use replaceActorsForAssignable.
817
+ var getIssueQuery struct {
818
+ Repository struct {
819
+ Issue struct {
820
+ ID githubv4.ID
821
+ Assignees struct {
822
+ Nodes []struct {
823
+ ID githubv4.ID
824
+ }
825
+ } `graphql:"assignees(first: 100)"`
826
+ } `graphql:"issue(number: $number)"`
827
+ } `graphql:"repository(owner: $owner, name: $name)"`
828
+ }
829
+
830
+ variables = map [string ]any {
831
+ "owner" : githubv4 .String (owner ),
832
+ "name" : githubv4 .String (repo ),
833
+ "number" : githubv4 .Int (issueNumber ), //nolint:gosec // G115: issueNumber is guaranteed to fit into int32
834
+ }
835
+
836
+ if err := client .Query (ctx , & getIssueQuery , variables ); err != nil {
837
+ return mcp .NewToolResultError (fmt .Sprintf ("failed to get issue ID: %v" , err )), nil
838
+ }
839
+
840
+ // Then, get all the current assignees because the only way to assign copilot is to use replaceActorsForAssignable
841
+ // which replaces all assignees.
842
+ var assignCopilotMutation struct {
843
+ ReplaceActorsForAssignable struct {
844
+ Assignable struct {
845
+ ID githubv4.ID
846
+ Title string
847
+ Assignees struct {
848
+ Nodes []struct {
849
+ Login string
850
+ }
851
+ } `graphql:"assignees(first: 10)"`
852
+ } `graphql:"... on Issue"`
853
+ } `graphql:"replaceActorsForAssignable(input: $input)"`
854
+ }
855
+
856
+ type ReplaceActorsForAssignableInput struct {
857
+ AssignableID githubv4.ID `json:"assignableId"`
858
+ ActorIDs []githubv4.ID `json:"actorIds"`
859
+ }
860
+
861
+ actorIDs := make ([]githubv4.ID , len (getIssueQuery .Repository .Issue .Assignees .Nodes )+ 1 )
862
+ for i , node := range getIssueQuery .Repository .Issue .Assignees .Nodes {
863
+ actorIDs [i ] = node .ID
864
+ }
865
+ actorIDs [len (getIssueQuery .Repository .Issue .Assignees .Nodes )] = copilotAssignee .ID
866
+
867
+ if err := client .Mutate (
868
+ ctx ,
869
+ & assignCopilotMutation ,
870
+ ReplaceActorsForAssignableInput {
871
+ AssignableID : getIssueQuery .Repository .Issue .ID ,
872
+ ActorIDs : actorIDs ,
873
+ },
874
+ nil ,
875
+ ); err != nil {
876
+ return nil , fmt .Errorf ("failed to replace actors for assignable: %w" , err )
877
+ }
878
+
879
+ r , err := json .Marshal (assignCopilotMutation .ReplaceActorsForAssignable .Assignable )
880
+ if err != nil {
881
+ return nil , fmt .Errorf ("failed to marshal response: %w" , err )
882
+ }
883
+
884
+ return mcp .NewToolResultText (string (r )), nil
885
+ }
886
+ }
887
+
714
888
// parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object.
715
889
// Returns the parsed time or an error if parsing fails.
716
890
// Example formats supported: "2023-01-15T14:30:00Z", "2023-01-15"
0 commit comments