Skip to content

Commit 908e3b0

Browse files
committed
Issue github#196: Document add_sub_issue tool functionality
1 parent f966e30 commit 908e3b0

4 files changed

Lines changed: 508 additions & 2 deletions

File tree

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,13 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
232232
- `issue_number`: Issue number (number, required)
233233
- `body`: Comment text (string, required)
234234

235+
- **add_sub_issue** - Add a sub-issue to an existing issue
236+
237+
- `owner`: Repository owner (string, required)
238+
- `repo`: Repository name (string, required)
239+
- `parent_issue_number`: Parent issue number (number, required)
240+
- `child_issue_number`: Child issue number to add as sub-issue (number, required)
241+
235242
- **list_issues** - List and filter repository issues
236243

237244
- `owner`: Repository owner (string, required)
@@ -509,5 +516,4 @@ The exported Go API of this module should currently be considered unstable, and
509516

510517
## License
511518

512-
This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE) for the full terms.
513-
519+
This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE) for the full terms.

pkg/github/issues.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
1824
func 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

Comments
 (0)