From e3a96a4296aa02c259ef8751565dc1b4a78efc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cyashdesai30=E2=80=9D?= <“yashdesai3011@gmail.comgit config --global user.email “yashdesai3011@gmail.com> Date: Fri, 1 May 2026 13:20:45 +0530 Subject: [PATCH] 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 --- README.md | 1 + .../__toolsnaps__/create_pull_request.snap | 7 ++++ pkg/github/helper_test.go | 1 + pkg/github/pullrequests.go | 25 +++++++++++ pkg/github/pullrequests_test.go | 41 +++++++++++++++++++ ui/src/apps/pr-write/App.tsx | 22 +++++++++- 6 files changed, 96 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f9baa780e..dd5c0341ff 100644 --- a/README.md +++ b/README.md @@ -1065,6 +1065,7 @@ The following sets of tools are available: - `body`: PR description (string, optional) - `draft`: Create as draft PR (boolean, optional) - `head`: Branch containing changes (string, required) + - `labels`: Labels to apply to this pull request (string[], optional) - `maintainer_can_modify`: Allow maintainer edits (boolean, optional) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) diff --git a/pkg/github/__toolsnaps__/create_pull_request.snap b/pkg/github/__toolsnaps__/create_pull_request.snap index a8a94ce690..ea0fc0398d 100644 --- a/pkg/github/__toolsnaps__/create_pull_request.snap +++ b/pkg/github/__toolsnaps__/create_pull_request.snap @@ -30,6 +30,13 @@ "description": "Branch containing changes", "type": "string" }, + "labels": { + "description": "Labels to apply to this pull request", + "items": { + "type": "string" + }, + "type": "array" + }, "maintainer_can_modify": { "description": "Allow maintainer edits", "type": "boolean" diff --git a/pkg/github/helper_test.go b/pkg/github/helper_test.go index ff752f5f37..3c8a11d249 100644 --- a/pkg/github/helper_test.go +++ b/pkg/github/helper_test.go @@ -59,6 +59,7 @@ const ( PostReposIssuesByOwnerByRepo = "POST /repos/{owner}/{repo}/issues" PostReposIssuesCommentsByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/comments" PatchReposIssuesByOwnerByRepoByIssueNumber = "PATCH /repos/{owner}/{repo}/issues/{issue_number}" + PostReposIssuesLabelsByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/labels" GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber = "GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues" PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues" DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber = "DELETE /repos/{owner}/{repo}/issues/{issue_number}/sub_issue" diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 9c2a098755..7007ed19f3 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -586,6 +586,11 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo Type: "boolean", Description: "Allow maintainer edits", }, + "labels": { + Type: "array", + Items: &jsonschema.Schema{Type: "string"}, + Description: "Labels to apply to this pull request", + }, }, Required: []string{"owner", "repo", "title", "head", "base"}, }, @@ -648,6 +653,11 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo return utils.NewToolResultError(err.Error()), nil, nil } + labels, err := OptionalStringArrayParam(args, "labels") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + newPR := &github.NewPullRequest{ Title: github.Ptr(title), Head: github.Ptr(head), @@ -683,6 +693,21 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to create pull request", resp, bodyBytes), nil, nil } + // Add labels if provided + if len(labels) > 0 { + _, labelsResp, labelsErr := client.Issues.AddLabelsToIssue(ctx, owner, repo, pr.GetNumber(), labels) + if labelsErr != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("pull request created (#%d) but failed to add labels", pr.GetNumber()), + labelsResp, + labelsErr, + ), nil, nil + } + if labelsResp != nil { + _ = labelsResp.Body.Close() + } + } + // Return minimal response with just essential information minimalResponse := MinimalResponse{ ID: fmt.Sprintf("%d", pr.GetID()), diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 4f0ec9493b..57018ed314 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -2181,6 +2181,7 @@ func Test_CreatePullRequest(t *testing.T) { assert.Contains(t, schema.Properties, "base") assert.Contains(t, schema.Properties, "draft") assert.Contains(t, schema.Properties, "maintainer_can_modify") + assert.Contains(t, schema.Properties, "labels") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "title", "head", "base"}) // Setup mock PR for success case @@ -2269,6 +2270,46 @@ func Test_CreatePullRequest(t *testing.T) { expectError: true, expectedErrMsg: "failed to create pull request", }, + { + name: "successful PR creation with labels", + mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + PostReposPullsByOwnerByRepo: mockResponse(t, http.StatusCreated, mockPR), + PostReposIssuesLabelsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, []*github.Label{ + {Name: github.Ptr("bug")}, + {Name: github.Ptr("enhancement")}, + }), + }), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "title": "Test PR", + "head": "feature-branch", + "base": "main", + "labels": []any{"bug", "enhancement"}, + }, + expectError: false, + expectedPR: mockPR, + }, + { + name: "labels addition fails after PR creation", + mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + PostReposPullsByOwnerByRepo: mockResponse(t, http.StatusCreated, mockPR), + PostReposIssuesLabelsByOwnerByRepoByIssueNumber: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Label does not exist"}`)) + }), + }), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "title": "Test PR", + "head": "feature-branch", + "base": "main", + "labels": []any{"nonexistent-label"}, + }, + expectError: true, + expectedErrMsg: "failed to add labels", + }, } for _, tc := range tests { diff --git a/ui/src/apps/pr-write/App.tsx b/ui/src/apps/pr-write/App.tsx index f5ddbdf29d..53fc8149fa 100644 --- a/ui/src/apps/pr-write/App.tsx +++ b/ui/src/apps/pr-write/App.tsx @@ -119,6 +119,7 @@ function SuccessView({ function CreatePRApp() { const [title, setTitle] = useState(""); const [body, setBody] = useState(""); + const [labels, setLabels] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [successPR, setSuccessPR] = useState(null); @@ -140,6 +141,10 @@ function CreatePRApp() { useEffect(() => { if (toolInput?.title) setTitle(toolInput.title as string); if (toolInput?.body) setBody(toolInput.body as string); + if (toolInput?.labels) { + const labelsArr = toolInput.labels as string[]; + setLabels(labelsArr.join(", ")); + } if (toolInput?.draft) setIsDraft(toolInput.draft as boolean); if (toolInput?.maintainer_can_modify !== undefined) { setMaintainerCanModify(toolInput.maintainer_can_modify as boolean); @@ -154,6 +159,8 @@ function CreatePRApp() { setError(null); setSubmittedTitle(title); + const labelsArray = labels.split(",").map(l => l.trim()).filter(l => l !== ""); + try { const result = await callTool("create_pull_request", { owner, repo, @@ -163,6 +170,7 @@ function CreatePRApp() { base, draft: isDraft, maintainer_can_modify: maintainerCanModify, + labels: labelsArray, _ui_submitted: true }); @@ -182,7 +190,7 @@ function CreatePRApp() { } finally { setIsSubmitting(false); } - }, [title, body, owner, repo, head, base, isDraft, maintainerCanModify, callTool]); + }, [title, body, labels, owner, repo, head, base, isDraft, maintainerCanModify, callTool]); if (successPR) { return ( @@ -260,6 +268,18 @@ function CreatePRApp() { /> + {/* Labels */} + + Labels + setLabels(e.target.value)} + placeholder="bug, enhancement, help wanted (comma separated)" + block + contrast + /> + + {/* Description */}