Skip to content

Commit e3a96a4

Browse files
author
“yashdesai30”
committed
feat(pullrequests): add labels support to create_pull_request tool
Add optional 'labels' parameter to the create_pull_request tool, enabling users to apply labels during PR creation. Since the GitHub REST API doesn't support labels in the PR creation endpoint, labels are applied in a second step via the Issues API (POST /repos/{owner}/{repo}/issues/{issue_number}/labels). Changes: - Add 'labels' array property to CreatePullRequest InputSchema - Add post-creation AddLabelsToIssue call in the handler - Add labels input field to pr-write MCP App UI - Add test cases for label success and failure scenarios - Update toolsnap snapshot and README documentation
1 parent 926d049 commit e3a96a4

6 files changed

Lines changed: 96 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,7 @@ The following sets of tools are available:
10651065
- `body`: PR description (string, optional)
10661066
- `draft`: Create as draft PR (boolean, optional)
10671067
- `head`: Branch containing changes (string, required)
1068+
- `labels`: Labels to apply to this pull request (string[], optional)
10681069
- `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
10691070
- `owner`: Repository owner (string, required)
10701071
- `repo`: Repository name (string, required)

pkg/github/__toolsnaps__/create_pull_request.snap

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@
3030
"description": "Branch containing changes",
3131
"type": "string"
3232
},
33+
"labels": {
34+
"description": "Labels to apply to this pull request",
35+
"items": {
36+
"type": "string"
37+
},
38+
"type": "array"
39+
},
3340
"maintainer_can_modify": {
3441
"description": "Allow maintainer edits",
3542
"type": "boolean"

pkg/github/helper_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const (
5959
PostReposIssuesByOwnerByRepo = "POST /repos/{owner}/{repo}/issues"
6060
PostReposIssuesCommentsByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/comments"
6161
PatchReposIssuesByOwnerByRepoByIssueNumber = "PATCH /repos/{owner}/{repo}/issues/{issue_number}"
62+
PostReposIssuesLabelsByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/labels"
6263
GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber = "GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues"
6364
PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues"
6465
DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber = "DELETE /repos/{owner}/{repo}/issues/{issue_number}/sub_issue"

pkg/github/pullrequests.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,11 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
586586
Type: "boolean",
587587
Description: "Allow maintainer edits",
588588
},
589+
"labels": {
590+
Type: "array",
591+
Items: &jsonschema.Schema{Type: "string"},
592+
Description: "Labels to apply to this pull request",
593+
},
589594
},
590595
Required: []string{"owner", "repo", "title", "head", "base"},
591596
},
@@ -648,6 +653,11 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
648653
return utils.NewToolResultError(err.Error()), nil, nil
649654
}
650655

656+
labels, err := OptionalStringArrayParam(args, "labels")
657+
if err != nil {
658+
return utils.NewToolResultError(err.Error()), nil, nil
659+
}
660+
651661
newPR := &github.NewPullRequest{
652662
Title: github.Ptr(title),
653663
Head: github.Ptr(head),
@@ -683,6 +693,21 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
683693
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to create pull request", resp, bodyBytes), nil, nil
684694
}
685695

696+
// Add labels if provided
697+
if len(labels) > 0 {
698+
_, labelsResp, labelsErr := client.Issues.AddLabelsToIssue(ctx, owner, repo, pr.GetNumber(), labels)
699+
if labelsErr != nil {
700+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
701+
fmt.Sprintf("pull request created (#%d) but failed to add labels", pr.GetNumber()),
702+
labelsResp,
703+
labelsErr,
704+
), nil, nil
705+
}
706+
if labelsResp != nil {
707+
_ = labelsResp.Body.Close()
708+
}
709+
}
710+
686711
// Return minimal response with just essential information
687712
minimalResponse := MinimalResponse{
688713
ID: fmt.Sprintf("%d", pr.GetID()),

pkg/github/pullrequests_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2181,6 +2181,7 @@ func Test_CreatePullRequest(t *testing.T) {
21812181
assert.Contains(t, schema.Properties, "base")
21822182
assert.Contains(t, schema.Properties, "draft")
21832183
assert.Contains(t, schema.Properties, "maintainer_can_modify")
2184+
assert.Contains(t, schema.Properties, "labels")
21842185
assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "title", "head", "base"})
21852186

21862187
// Setup mock PR for success case
@@ -2269,6 +2270,46 @@ func Test_CreatePullRequest(t *testing.T) {
22692270
expectError: true,
22702271
expectedErrMsg: "failed to create pull request",
22712272
},
2273+
{
2274+
name: "successful PR creation with labels",
2275+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
2276+
PostReposPullsByOwnerByRepo: mockResponse(t, http.StatusCreated, mockPR),
2277+
PostReposIssuesLabelsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, []*github.Label{
2278+
{Name: github.Ptr("bug")},
2279+
{Name: github.Ptr("enhancement")},
2280+
}),
2281+
}),
2282+
requestArgs: map[string]any{
2283+
"owner": "owner",
2284+
"repo": "repo",
2285+
"title": "Test PR",
2286+
"head": "feature-branch",
2287+
"base": "main",
2288+
"labels": []any{"bug", "enhancement"},
2289+
},
2290+
expectError: false,
2291+
expectedPR: mockPR,
2292+
},
2293+
{
2294+
name: "labels addition fails after PR creation",
2295+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
2296+
PostReposPullsByOwnerByRepo: mockResponse(t, http.StatusCreated, mockPR),
2297+
PostReposIssuesLabelsByOwnerByRepoByIssueNumber: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
2298+
w.WriteHeader(http.StatusNotFound)
2299+
_, _ = w.Write([]byte(`{"message": "Label does not exist"}`))
2300+
}),
2301+
}),
2302+
requestArgs: map[string]any{
2303+
"owner": "owner",
2304+
"repo": "repo",
2305+
"title": "Test PR",
2306+
"head": "feature-branch",
2307+
"base": "main",
2308+
"labels": []any{"nonexistent-label"},
2309+
},
2310+
expectError: true,
2311+
expectedErrMsg: "failed to add labels",
2312+
},
22722313
}
22732314

22742315
for _, tc := range tests {

ui/src/apps/pr-write/App.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ function SuccessView({
119119
function CreatePRApp() {
120120
const [title, setTitle] = useState("");
121121
const [body, setBody] = useState("");
122+
const [labels, setLabels] = useState("");
122123
const [isSubmitting, setIsSubmitting] = useState(false);
123124
const [error, setError] = useState<string | null>(null);
124125
const [successPR, setSuccessPR] = useState<PRResult | null>(null);
@@ -140,6 +141,10 @@ function CreatePRApp() {
140141
useEffect(() => {
141142
if (toolInput?.title) setTitle(toolInput.title as string);
142143
if (toolInput?.body) setBody(toolInput.body as string);
144+
if (toolInput?.labels) {
145+
const labelsArr = toolInput.labels as string[];
146+
setLabels(labelsArr.join(", "));
147+
}
143148
if (toolInput?.draft) setIsDraft(toolInput.draft as boolean);
144149
if (toolInput?.maintainer_can_modify !== undefined) {
145150
setMaintainerCanModify(toolInput.maintainer_can_modify as boolean);
@@ -154,6 +159,8 @@ function CreatePRApp() {
154159
setError(null);
155160
setSubmittedTitle(title);
156161

162+
const labelsArray = labels.split(",").map(l => l.trim()).filter(l => l !== "");
163+
157164
try {
158165
const result = await callTool("create_pull_request", {
159166
owner, repo,
@@ -163,6 +170,7 @@ function CreatePRApp() {
163170
base,
164171
draft: isDraft,
165172
maintainer_can_modify: maintainerCanModify,
173+
labels: labelsArray,
166174
_ui_submitted: true
167175
});
168176

@@ -182,7 +190,7 @@ function CreatePRApp() {
182190
} finally {
183191
setIsSubmitting(false);
184192
}
185-
}, [title, body, owner, repo, head, base, isDraft, maintainerCanModify, callTool]);
193+
}, [title, body, labels, owner, repo, head, base, isDraft, maintainerCanModify, callTool]);
186194

187195
if (successPR) {
188196
return (
@@ -260,6 +268,18 @@ function CreatePRApp() {
260268
/>
261269
</FormControl>
262270

271+
{/* Labels */}
272+
<FormControl sx={{ mb: 3 }}>
273+
<FormControl.Label sx={{ fontWeight: "semibold" }}>Labels</FormControl.Label>
274+
<TextInput
275+
value={labels}
276+
onChange={(e) => setLabels(e.target.value)}
277+
placeholder="bug, enhancement, help wanted (comma separated)"
278+
block
279+
contrast
280+
/>
281+
</FormControl>
282+
263283
{/* Description */}
264284
<Box sx={{ mb: 3 }}>
265285
<Text as="label" sx={{ fontWeight: "semibold", fontSize: 1, display: "block", mb: 2 }}>

0 commit comments

Comments
 (0)