diff --git a/README.md b/README.md index ce27bdb06..e4543ecf5 100644 --- a/README.md +++ b/README.md @@ -533,6 +533,7 @@ The following sets of tools are available (all are on by default): - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `title`: Issue title (string, required) + - `type`: Type of this issue (string, optional) - **get_issue** - Get issue details - `issue_number`: The number of the issue (number, required) @@ -600,6 +601,7 @@ The following sets of tools are available (all are on by default): - `repo`: Repository name (string, required) - `state`: New state (string, optional) - `title`: New title (string, optional) + - `type`: New issue type (string, optional) diff --git a/pkg/github/__toolsnaps__/create_issue.snap b/pkg/github/__toolsnaps__/create_issue.snap index f065b0183..d11c41c0e 100644 --- a/pkg/github/__toolsnaps__/create_issue.snap +++ b/pkg/github/__toolsnaps__/create_issue.snap @@ -39,6 +39,10 @@ "title": { "description": "Issue title", "type": "string" + }, + "type": { + "description": "Type of this issue", + "type": "string" } }, "required": [ diff --git a/pkg/github/__toolsnaps__/update_issue.snap b/pkg/github/__toolsnaps__/update_issue.snap index 4bcae7ba7..d95579159 100644 --- a/pkg/github/__toolsnaps__/update_issue.snap +++ b/pkg/github/__toolsnaps__/update_issue.snap @@ -51,6 +51,10 @@ "title": { "description": "New title", "type": "string" + }, + "type": { + "description": "New issue type", + "type": "string" } }, "required": [ diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 3a1440489..80fe22f9d 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -553,39 +553,39 @@ func RemoveSubIssue(getClient GetClientFn, t translations.TranslationHelperFunc) } client, err := getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - - subIssueRequest := github.SubIssueRequest{ - SubIssueID: int64(subIssueID), - } - - subIssue, resp, err := client.SubIssue.Remove(ctx, owner, repo, int64(issueNumber), subIssueRequest) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to remove sub-issue", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to remove sub-issue: %s", string(body))), nil - } - - r, err := json.Marshal(subIssue) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil - } + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + subIssueRequest := github.SubIssueRequest{ + SubIssueID: int64(subIssueID), + } + + subIssue, resp, err := client.SubIssue.Remove(ctx, owner, repo, int64(issueNumber), subIssueRequest) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to remove sub-issue", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return mcp.NewToolResultError(fmt.Sprintf("failed to remove sub-issue: %s", string(body))), nil + } + + r, err := json.Marshal(subIssue) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } } // ReprioritizeSubIssue creates a tool to reprioritize a sub-issue to a different position in the parent list. @@ -788,6 +788,9 @@ func CreateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t mcp.WithNumber("milestone", mcp.Description("Milestone number"), ), + mcp.WithString("type", + mcp.Description("Type of this issue"), + ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](request, "owner") @@ -832,6 +835,12 @@ func CreateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t milestoneNum = &milestone } + // Get optional type + issueType, err := OptionalParam[string](request, "type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + // Create the issue request issueRequest := &github.IssueRequest{ Title: github.Ptr(title), @@ -841,6 +850,10 @@ func CreateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t Milestone: milestoneNum, } + if issueType != "" { + issueRequest.Type = github.Ptr(issueType) + } + client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) @@ -1129,6 +1142,9 @@ func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t mcp.WithNumber("milestone", mcp.Description("New milestone number"), ), + mcp.WithString("type", + mcp.Description("New issue type"), + ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](request, "owner") @@ -1199,6 +1215,15 @@ func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t issueRequest.Milestone = &milestoneNum } + // Get issue type + issueType, err := OptionalParam[string](request, "type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + if issueType != "" { + issueRequest.Type = github.Ptr(issueType) + } + client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 249fadef8..7c4983c64 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -581,6 +581,7 @@ func Test_CreateIssue(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "assignees") assert.Contains(t, tool.InputSchema.Properties, "labels") assert.Contains(t, tool.InputSchema.Properties, "milestone") + assert.Contains(t, tool.InputSchema.Properties, "type") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "title"}) // Setup mock issue for success case @@ -593,6 +594,7 @@ func Test_CreateIssue(t *testing.T) { Assignees: []*github.User{{Login: github.Ptr("user1")}, {Login: github.Ptr("user2")}}, Labels: []*github.Label{{Name: github.Ptr("bug")}, {Name: github.Ptr("help wanted")}}, Milestone: &github.Milestone{Number: github.Ptr(5)}, + Type: &github.IssueType{Name: github.Ptr("Bug")}, } tests := []struct { @@ -614,6 +616,7 @@ func Test_CreateIssue(t *testing.T) { "labels": []any{"bug", "help wanted"}, "assignees": []any{"user1", "user2"}, "milestone": float64(5), + "type": "Bug", }).andThen( mockResponse(t, http.StatusCreated, mockIssue), ), @@ -627,6 +630,7 @@ func Test_CreateIssue(t *testing.T) { "assignees": []any{"user1", "user2"}, "labels": []any{"bug", "help wanted"}, "milestone": float64(5), + "type": "Bug", }, expectError: false, expectedIssue: mockIssue, @@ -722,6 +726,10 @@ func Test_CreateIssue(t *testing.T) { assert.Equal(t, *tc.expectedIssue.Body, *returnedIssue.Body) } + if tc.expectedIssue.Type != nil { + assert.Equal(t, *tc.expectedIssue.Type.Name, *returnedIssue.Type.Name) + } + // Check assignees if expected if len(tc.expectedIssue.Assignees) > 0 { assert.Equal(t, len(tc.expectedIssue.Assignees), len(returnedIssue.Assignees)) @@ -1066,6 +1074,7 @@ func Test_UpdateIssue(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "labels") assert.Contains(t, tool.InputSchema.Properties, "assignees") assert.Contains(t, tool.InputSchema.Properties, "milestone") + assert.Contains(t, tool.InputSchema.Properties, "type") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "issue_number"}) // Setup mock issue for success case @@ -1078,6 +1087,7 @@ func Test_UpdateIssue(t *testing.T) { Assignees: []*github.User{{Login: github.Ptr("assignee1")}, {Login: github.Ptr("assignee2")}}, Labels: []*github.Label{{Name: github.Ptr("bug")}, {Name: github.Ptr("priority")}}, Milestone: &github.Milestone{Number: github.Ptr(5)}, + Type: &github.IssueType{Name: github.Ptr("Bug")}, } tests := []struct { @@ -1100,6 +1110,7 @@ func Test_UpdateIssue(t *testing.T) { "labels": []any{"bug", "priority"}, "assignees": []any{"assignee1", "assignee2"}, "milestone": float64(5), + "type": "Bug", }).andThen( mockResponse(t, http.StatusOK, mockIssue), ), @@ -1115,6 +1126,7 @@ func Test_UpdateIssue(t *testing.T) { "labels": []any{"bug", "priority"}, "assignees": []any{"assignee1", "assignee2"}, "milestone": float64(5), + "type": "Bug", }, expectError: false, expectedIssue: mockIssue, @@ -1126,9 +1138,10 @@ func Test_UpdateIssue(t *testing.T) { mock.PatchReposIssuesByOwnerByRepoByIssueNumber, mockResponse(t, http.StatusOK, &github.Issue{ Number: github.Ptr(123), - Title: github.Ptr("Only Title Updated"), + Title: github.Ptr("Updated Issue Title"), HTMLURL: github.Ptr("https://github.com/owner/repo/issues/123"), State: github.Ptr("open"), + Type: &github.IssueType{Name: github.Ptr("Feature")}, }), ), ), @@ -1136,14 +1149,16 @@ func Test_UpdateIssue(t *testing.T) { "owner": "owner", "repo": "repo", "issue_number": float64(123), - "title": "Only Title Updated", + "title": "Updated Issue Title", + "type": "Feature", }, expectError: false, expectedIssue: &github.Issue{ Number: github.Ptr(123), - Title: github.Ptr("Only Title Updated"), + Title: github.Ptr("Updated Issue Title"), HTMLURL: github.Ptr("https://github.com/owner/repo/issues/123"), State: github.Ptr("open"), + Type: &github.IssueType{Name: github.Ptr("Feature")}, }, }, { @@ -1232,6 +1247,10 @@ func Test_UpdateIssue(t *testing.T) { assert.Equal(t, *tc.expectedIssue.Body, *returnedIssue.Body) } + if tc.expectedIssue.Type != nil { + assert.Equal(t, *tc.expectedIssue.Type.Name, *returnedIssue.Type.Name) + } + // Check assignees if expected if len(tc.expectedIssue.Assignees) > 0 { assert.Len(t, returnedIssue.Assignees, len(tc.expectedIssue.Assignees))