FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(issues): add batch_update_issue_labels granular tool by CAOShurong · Pull Request #3149 · github/github-mcp-server · GitHub

Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .go  (5) .snap  (1) All 2 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"annotations": {
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": true,
"readOnlyHint": false,
"title": "Batch Update Issue Labels"
},
"description": "Apply label changes to multiple issues in one call. Each entry in operations targets one issue and can add labels, remove labels, or both. Labels not named in the operation are left untouched. Invalid input fails the whole call before any change is applied; per-issue API errors are reported individually.",
"inputSchema": {
"properties": {
"operations": {
"description": "One entry per issue, in any order. Each entry requires issue_number plus at least one non-empty of add or remove (arrays of label names). Duplicate issue numbers are rejected.",
"items": {
"additionalProperties": false,
"properties": {
"add": {
"description": "Label names to add to this issue (GitHub creates missing labels implicitly)",
"items": {
"minLength": 1,
"type": "string"
},
"type": "array"
},
"issue_number": {
"description": "The issue number to update",
"minimum": 1,
"type": "number"
},
"remove": {
"description": "Label names to remove from this issue",
"items": {
"minLength": 1,
"type": "string"
},
"type": "array"
}
},
"required": [
"issue_number"
],
"type": "object"
},
"minItems": 1,
"type": "array"
},
"owner": {
"description": "Repository owner (username or organization)",
"type": "string"
},
"repo": {
"description": "Repository name",
"type": "string"
}
},
"required": [
"owner",
"repo",
"operations"
],
"type": "object"
},
"name": "batch_update_issue_labels"
}
156 changes: 156 additions & 0 deletions pkg/github/batch_labels_granular_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package github

import (
"context"
"net/http"
"strings"
"testing"

"github.com/github/github-mcp-server/pkg/translations"
gogithub "github.com/google/go-github/v89/github"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGranularBatchUpdateIssueLabels(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectToolErr bool
expectedErrMsg string
}{
{
name: "add and remove across multiple issues",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposIssuesByOwnerByRepoByIssueNumberLabels: expectRequestBody(t, []any{"bug", "priority/high"}).
andThen(mockResponse(t, http.StatusOK, []*gogithub.Label{
{Name: "bug"},
{Name: "priority/high"},
})),
DeleteReposIssuesByOwnerByRepoByIssueNumberLabel: mockResponse(t, http.StatusNoContent, nil),
}),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"operations": []any{
map[string]any{
"issue_number": float64(1),
"add": []any{"bug", "priority/high"},
},
map[string]any{
"issue_number": float64(2),
"remove": []any{"wontfix"},
},
},
},
expectToolErr: false,
},
{
name: "empty operations array rejected",
mockedClient: MockHTTPClientWithHandlers(nil),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"operations": []any{},
},
expectToolErr: true,
expectedErrMsg: "operations must contain at least one entry",
},
{
name: "missing operations parameter rejected",
mockedClient: MockHTTPClientWithHandlers(nil),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
},
expectToolErr: true,
expectedErrMsg: "missing required parameter: operations",
},
{
name: "operation without add or remove rejected",
mockedClient: MockHTTPClientWithHandlers(nil),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"operations": []any{
map[string]any{"issue_number": float64(1)},
},
},
expectToolErr: true,
expectedErrMsg: "at least one non-empty of add or remove is required",
},
{
name: "duplicate issue numbers rejected",
mockedClient: MockHTTPClientWithHandlers(nil),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"operations": []any{
map[string]any{"issue_number": float64(1), "add": []any{"bug"}},
map[string]any{"issue_number": float64(1), "remove": []any{"wontfix"}},
},
},
expectToolErr: true,
expectedErrMsg: "duplicate issue_number 1",
},
{
name: "empty label name in add rejected",
mockedClient: MockHTTPClientWithHandlers(nil),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"operations": []any{
map[string]any{"issue_number": float64(1), "add": []any{""}},
},
},
expectToolErr: true,
expectedErrMsg: "add contains an empty label name",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
deps := BaseDeps{Client: mustNewGHClient(t, tc.mockedClient)}
serverTool := GranularBatchUpdateIssueLabels(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)

request := createMCPRequest(tc.requestArgs)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
if tc.expectToolErr {
errorContent := getErrorResult(t, result)
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
return
}
assert.False(t, result.IsError)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, `"issue_number":1`)
assert.Contains(t, textContent.Text, `"applied":true`)
})
}
}

func TestGranularBatchUpdateIssueLabelsPartialFailure(t *testing.T) {
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposIssuesByOwnerByRepoByIssueNumberLabels: func(_ http.ResponseWriter, _ *http.Request) {},
}))
deps := BaseDeps{Client: client}
serverTool := GranularBatchUpdateIssueLabels(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)

request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"operations": []any{
map[string]any{"issue_number": float64(1), "add": []any{"bug"}},
},
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
// A per-issue API failure must surface as a tool error with a JSON body
// identifying the failing issue.
assert.True(t, result.IsError, "expected IsError on API failure")
textContent := getTextResult(t, result)
assert.True(t, strings.Contains(textContent.Text, "add failed"))
}
2 changes: 2 additions & 0 deletions pkg/github/granular_tools_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func TestGranularToolSnaps(t *testing.T) {
GranularUpdateIssueBody,
GranularUpdateIssueAssignees,
GranularUpdateIssueLabels,
GranularBatchUpdateIssueLabels,
GranularUpdateIssueMilestone,
GranularUpdateIssueType,
GranularUpdateIssueState,
Expand Down Expand Up @@ -84,6 +85,7 @@ func TestIssuesGranularToolset(t *testing.T) {
"update_issue_body",
"update_issue_assignees",
"update_issue_labels",
"batch_update_issue_labels",
"update_issue_milestone",
"update_issue_type",
"update_issue_state",
Expand Down
2 changes: 2 additions & 0 deletions pkg/github/helper_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ const (
GetReposIssuesCommentsByOwnerByRepoByIssueNumber = "GET /repos/{owner}/{repo}/issues/{issue_number}/comments"
PostReposIssuesByOwnerByRepo = "POST /repos/{owner}/{repo}/issues"
PostReposIssuesCommentsByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/comments"
PostReposIssuesByOwnerByRepoByIssueNumberLabels = "POST /repos/{owner}/{repo}/issues/{issue_number}/labels"
DeleteReposIssuesByOwnerByRepoByIssueNumberLabel = "DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}"
PostReposIssuesReactionsByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/reactions"
PatchReposIssuesByOwnerByRepoByIssueNumber = "PATCH /repos/{owner}/{repo}/issues/{issue_number}"
GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber = "GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues"
Expand Down
Loading

Back | FazBrowse Home | New Git URL