Repository: microsoft/azure-devops-mcp

Issue:
Add label support to update_pull_request tool
## Problem Statement

Currently, the `update_pull_request` tool doesn't support updating labels on existing pull requests in Azure DevOps. While we recently added label support to `create_pull_request`, users cannot modify labels after a PR has been created through the MCP tools.

## Proposed Solution

Add an optional `labels` parameter to the `update_pull_request` tool that accepts an array of label names. The behavior should follow the same pattern as other optional update fields:

- **`undefined`** (not provided): Don't modify existing labels
- **`[]`** (empty array): Remove all labels from the PR
- **`["label1", "label2"]`**: Replace existing labels with the specified ones

## Implementation Approach

Similar to the `create_pull_request` implementation:

1. Add an optional `labels` parameter to the `update_pull_request` tool schema (array of strings, optional)
2. Map the label names to `WebApiTagDefinition[]` format when provided: `labels.map((label) => ({ name: label }))`
3. Only include labels in the update request if the parameter is defined:
   ```typescript
   if (labels !== undefined) {
     updateRequest.labels = labels.map((label) => ({ name: label }));
   }
   ```
4. Add comprehensive tests to verify:
   - Labels are not updated when parameter is undefined
   - All labels are removed when empty array is provided
   - Labels are replaced when array with values is provided

## Benefits

- Completes the label management story for pull requests
- Consistent API pattern with other update fields (title, description, etc.)
- Single API call to update labels instead of separate operations
- Allows removing all labels by passing empty array

Discussion:
@Meir017: @nikolapeja6 any ETA for new release with this?@nikolapeja6: @Meir017 Sorry for the delayed response, I was out of office for the last 2-3 weeks. v2.4.0 that was just released should contain this change

Solution patch:
diff --git a/src/tools/repositories.ts b/src/tools/repositories.ts
index f49e04c..f89fa8c 100644
--- a/src/tools/repositories.ts
+++ b/src/tools/repositories.ts
@@ -24,6 +24,7 @@ import {
 import { z } from "zod";
 import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
 import { GitRepository } from "azure-devops-node-api/interfaces/TfvcInterfaces.js";
+import { WebApiTagDefinition } from "azure-devops-node-api/interfaces/CoreInterfaces.js";
 import { getEnumKeys } from "../utils.js";
 
 const REPO_TOOLS = {
@@ -144,8 +145,9 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<stri
       isDraft: z.boolean().optional().default(false).describe("Indicates whether the pull request is a draft. Defaults to false."),
       workItems: z.string().optional().describe("Work item IDs to associate with the pull request, space-separated."),
       forkSourceRepositoryId: z.string().optional().describe("The ID of the fork repository that the pull request originates from. Optional, used when creating a pull request from a fork."),
+      labels: z.array(z.string()).optional().describe("Array of label names to add to the pull request after creation."),
     },
-    async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, workItems, forkSourceRepositoryId }) => {
+    async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, workItems, forkSourceRepositoryId, labels }) => {
       const connection = await connectionProvider();
       const gitApi = await connection.getGitApi();
       const workItemRefs = workItems ? workItems.split(" ").map((id) => ({ id: id.trim() })) : [];
@@ -158,6 +160,8 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<stri
           }
         : undefined;
 
+      const labelDefinitions: WebApiTagDefinition[] | undefined = labels ? labels.map((label) => ({ name: label })) : undefined;
+
       const pullRequest = await gitApi.createPullRequest(
         {
           sourceRefName,
@@ -167,6 +171,7 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<stri
           isDraft,
           workItemRefs: workItemRefs,
           forkSource,
+          labels: labelDefinitions,
         },
         repositoryId
       );



Test patch:
diff --git a/test/src/tools/repositories.test.ts b/test/src/tools/repositories.test.ts
index 2c6bb23..ae06bd8 100644
--- a/test/src/tools/repositories.test.ts
+++ b/test/src/tools/repositories.test.ts
@@ -675,7 +675,7 @@ describe("repos tools", () => {
       expect(result.content[0].text).toBe(JSON.stringify(expectedTrimmedPR, null, 2));
     });
 
-    it("should create pull request with all optional fields", async () => {
+    it("should create pull request with all optional fields including labels", async () => {
       configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider);
 
       const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request);
@@ -709,6 +709,7 @@ describe("repos tools", () => {
         isDraft: true,
         workItems: "1234 5678",
         forkSourceRepositoryId: "fork-repo-123",
+        labels: ["enhancement", "needs-review"],
       };
 
       const result = await handler(params);
@@ -726,6 +727,7 @@ describe("repos tools", () => {
               id: "fork-repo-123",
             },
           },
+          labels: [{ name: "enhancement" }, { name: "needs-review" }],
         },
         "repo123"
       );