@@ -11,9 +11,15 @@ import (
1111 "github.com/github/github-mcp-server/pkg/translations"
1212 "github.com/google/go-github/v69/github"
1313 "github.com/mark3labs/mcp-go/mcp"
14+ "github.com/mark3labs/mcp-go/mock"
1415 "github.com/mark3labs/mcp-go/server"
1516)
1617
18+ const (
19+ PostReposSubIssuesByOwnerByRepoByParentIssueNumberByChildIssueNumber mock.EndpointPattern = "POST /repos/{owner}/{repo}/issues/{parent_issue_number}/sub-issues/{child_issue_number}"
20+ GetReposSubIssuesByOwnerByRepoByIssueNumber mock.EndpointPattern = "GET /repos/{owner}/{repo}/issues/{issue_number}/sub-issues"
21+ )
22+
1723// GetIssue creates a tool to get details of a specific issue in a GitHub repository.
1824func GetIssue (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
1925 return mcp .NewTool ("get_issue" ,
@@ -49,6 +55,8 @@ func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool
4955 if err != nil {
5056 return nil , fmt .Errorf ("failed to get GitHub client: %w" , err )
5157 }
58+
59+ // Get issue details
5260 issue , resp , err := client .Issues .Get (ctx , owner , repo , issueNumber )
5361 if err != nil {
5462 return nil , fmt .Errorf ("failed to get issue: %w" , err )
@@ -63,6 +71,37 @@ func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool
6371 return mcp .NewToolResultError (fmt .Sprintf ("failed to get issue: %s" , string (body ))), nil
6472 }
6573
74+ // Get sub-issues
75+ url := fmt .Sprintf ("repos/%v/%v/issues/%d/sub-issues" , owner , repo , issueNumber )
76+ req , err := client .NewRequest ("GET" , url , nil )
77+ if err != nil {
78+ return nil , fmt .Errorf ("failed to create request: %w" , err )
79+ }
80+
81+ var subIssues []* github.Issue
82+ resp , err = client .Do (ctx , req , & subIssues )
83+ if err == nil && resp .StatusCode == http .StatusOK {
84+ // Only include sub-issues if the request was successful
85+ // Create a custom response struct that includes sub-issues
86+ type IssueWithSubIssues struct {
87+ * github.Issue
88+ SubIssues []* github.Issue `json:"sub_issues,omitempty"`
89+ }
90+
91+ issueWithSubs := & IssueWithSubIssues {
92+ Issue : issue ,
93+ SubIssues : subIssues ,
94+ }
95+
96+ r , err := json .Marshal (issueWithSubs )
97+ if err != nil {
98+ return nil , fmt .Errorf ("failed to marshal issue with sub-issues: %w" , err )
99+ }
100+
101+ return mcp .NewToolResultText (string (r )), nil
102+ }
103+
104+ // If getting sub-issues failed, just return the main issue
66105 r , err := json .Marshal (issue )
67106 if err != nil {
68107 return nil , fmt .Errorf ("failed to marshal issue: %w" , err )
@@ -683,6 +722,159 @@ func GetIssueComments(getClient GetClientFn, t translations.TranslationHelperFun
683722 }
684723}
685724
725+ // AddSubIssue creates a tool to add a sub-issue to an existing issue.
726+ func AddSubIssue (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
727+ return mcp .NewTool ("add_sub_issue" ,
728+ mcp .WithDescription (t ("TOOL_ADD_SUB_ISSUE_DESCRIPTION" , "Add a sub-issue to an existing issue" )),
729+ mcp .WithString ("owner" ,
730+ mcp .Required (),
731+ mcp .Description ("Repository owner" ),
732+ ),
733+ mcp .WithString ("repo" ,
734+ mcp .Required (),
735+ mcp .Description ("Repository name" ),
736+ ),
737+ mcp .WithNumber ("parent_issue_number" ,
738+ mcp .Required (),
739+ mcp .Description ("Parent issue number" ),
740+ ),
741+ mcp .WithNumber ("child_issue_number" ,
742+ mcp .Required (),
743+ mcp .Description ("Child issue number to add as sub-issue" ),
744+ ),
745+ ),
746+ func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
747+ owner , err := requiredParam [string ](request , "owner" )
748+ if err != nil {
749+ return mcp .NewToolResultError (err .Error ()), nil
750+ }
751+ repo , err := requiredParam [string ](request , "repo" )
752+ if err != nil {
753+ return mcp .NewToolResultError (err .Error ()), nil
754+ }
755+ parentIssueNumber , err := RequiredInt (request , "parent_issue_number" )
756+ if err != nil {
757+ return mcp .NewToolResultError (err .Error ()), nil
758+ }
759+ childIssueNumber , err := RequiredInt (request , "child_issue_number" )
760+ if err != nil {
761+ return mcp .NewToolResultError (err .Error ()), nil
762+ }
763+
764+ client , err := getClient (ctx )
765+ if err != nil {
766+ return nil , fmt .Errorf ("failed to get GitHub client: %w" , err )
767+ }
768+
769+ // First verify both issues exist
770+ _ , resp , err := client .Issues .Get (ctx , owner , repo , parentIssueNumber )
771+ if err != nil {
772+ return nil , fmt .Errorf ("failed to get parent issue: %w" , err )
773+ }
774+ if resp .StatusCode != http .StatusOK {
775+ return mcp .NewToolResultError ("parent issue not found" ), nil
776+ }
777+
778+ _ , resp , err = client .Issues .Get (ctx , owner , repo , childIssueNumber )
779+ if err != nil {
780+ return nil , fmt .Errorf ("failed to get child issue: %w" , err )
781+ }
782+ if resp .StatusCode != http .StatusOK {
783+ return mcp .NewToolResultError ("child issue not found" ), nil
784+ }
785+
786+ // Add sub-issue relationship
787+ url := fmt .Sprintf ("repos/%v/%v/issues/%d/sub-issues/%d" , owner , repo , parentIssueNumber , childIssueNumber )
788+ req , err := client .NewRequest ("POST" , url , nil )
789+ if err != nil {
790+ return nil , fmt .Errorf ("failed to create request: %w" , err )
791+ }
792+
793+ resp , err = client .Do (ctx , req , nil )
794+ if err != nil {
795+ return nil , fmt .Errorf ("failed to add sub-issue: %w" , err )
796+ }
797+ defer func () { _ = resp .Body .Close () }()
798+
799+ if resp .StatusCode != http .StatusCreated {
800+ body , err := io .ReadAll (resp .Body )
801+ if err != nil {
802+ return nil , fmt .Errorf ("failed to read response body: %w" , err )
803+ }
804+ return mcp .NewToolResultError (fmt .Sprintf ("failed to add sub-issue: %s" , string (body ))), nil
805+ }
806+
807+ return mcp .NewToolResultText (fmt .Sprintf ("Successfully added issue #%d as a sub-issue of #%d" , childIssueNumber , parentIssueNumber )), nil
808+ }
809+ }
810+
811+ // GetSubIssues creates a tool to get sub-issues of a specific issue.
812+ func GetSubIssues (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
813+ return mcp .NewTool ("get_sub_issues" ,
814+ mcp .WithDescription (t ("TOOL_GET_SUB_ISSUES_DESCRIPTION" , "Get sub-issues of a specific issue" )),
815+ mcp .WithString ("owner" ,
816+ mcp .Required (),
817+ mcp .Description ("Repository owner" ),
818+ ),
819+ mcp .WithString ("repo" ,
820+ mcp .Required (),
821+ mcp .Description ("Repository name" ),
822+ ),
823+ mcp .WithNumber ("issue_number" ,
824+ mcp .Required (),
825+ mcp .Description ("Parent issue number" ),
826+ ),
827+ ),
828+ func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
829+ owner , err := requiredParam [string ](request , "owner" )
830+ if err != nil {
831+ return mcp .NewToolResultError (err .Error ()), nil
832+ }
833+ repo , err := requiredParam [string ](request , "repo" )
834+ if err != nil {
835+ return mcp .NewToolResultError (err .Error ()), nil
836+ }
837+ issueNumber , err := RequiredInt (request , "issue_number" )
838+ if err != nil {
839+ return mcp .NewToolResultError (err .Error ()), nil
840+ }
841+
842+ client , err := getClient (ctx )
843+ if err != nil {
844+ return nil , fmt .Errorf ("failed to get GitHub client: %w" , err )
845+ }
846+
847+ // Get sub-issues
848+ url := fmt .Sprintf ("repos/%v/%v/issues/%d/sub-issues" , owner , repo , issueNumber )
849+ req , err := client .NewRequest ("GET" , url , nil )
850+ if err != nil {
851+ return nil , fmt .Errorf ("failed to create request: %w" , err )
852+ }
853+
854+ var subIssues []* github.Issue
855+ resp , err := client .Do (ctx , req , & subIssues )
856+ if err != nil {
857+ return nil , fmt .Errorf ("failed to get sub-issues: %w" , err )
858+ }
859+ defer func () { _ = resp .Body .Close () }()
860+
861+ if resp .StatusCode != http .StatusOK {
862+ body , err := io .ReadAll (resp .Body )
863+ if err != nil {
864+ return nil , fmt .Errorf ("failed to read response body: %w" , err )
865+ }
866+ return mcp .NewToolResultError (fmt .Sprintf ("failed to get sub-issues: %s" , string (body ))), nil
867+ }
868+
869+ r , err := json .Marshal (subIssues )
870+ if err != nil {
871+ return nil , fmt .Errorf ("failed to marshal sub-issues: %w" , err )
872+ }
873+
874+ return mcp .NewToolResultText (string (r )), nil
875+ }
876+ }
877+
686878// parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object.
687879// Returns the parsed time or an error if parsing fails.
688880// Example formats supported: "2023-01-15T14:30:00Z", "2023-01-15"
0 commit comments