Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/tools/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,9 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<stri
deleteSourceBranch: z.boolean().optional().default(false).describe("Whether to delete the source branch when the pull request autocompletes. Defaults to false."),
transitionWorkItems: z.boolean().optional().default(true).describe("Whether to transition associated work items to the next state when the pull request autocompletes. Defaults to true."),
bypassReason: z.string().optional().describe("Reason for bypassing branch policies. When provided, branch policies will be automatically bypassed during autocompletion."),
labels: z.array(z.string()).optional().describe("Array of label names to replace existing labels. Undefined = no change, empty array = remove all labels, array with values = replace labels."),
},
async ({ repositoryId, pullRequestId, title, description, isDraft, targetRefName, status, autoComplete, mergeStrategy, deleteSourceBranch, transitionWorkItems, bypassReason }) => {
async ({ repositoryId, pullRequestId, title, description, isDraft, targetRefName, status, autoComplete, mergeStrategy, deleteSourceBranch, transitionWorkItems, bypassReason, labels }) => {
const connection = await connectionProvider();
const gitApi = await connection.getGitApi();

Expand All @@ -312,6 +313,10 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<stri
updateRequest.status = status === "Active" ? PullRequestStatus.Active.valueOf() : PullRequestStatus.Abandoned.valueOf();
}

if (labels !== undefined) {
updateRequest.labels = labels.map((label) => ({ name: label }));
}

if (autoComplete !== undefined) {
if (autoComplete) {
const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
Expand Down
125 changes: 125 additions & 0 deletions test/src/tools/repositories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,131 @@ describe("repos tools", () => {
// Validation should fail due to description being too long
await expect(handler(params)).rejects.toThrow();
});

it("should not update labels when labels parameter is undefined", async () => {
configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider);

const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request);
if (!call) throw new Error("repo_update_pull_request tool not registered");
const [, , , handler] = call;

const mockUpdatedPR = {
pullRequestId: 123,
codeReviewId: 123,
repository: { name: "test-repo" },
status: PullRequestStatus.Active,
createdBy: {
displayName: "Test User",
uniqueName: "[email protected]",
},
creationDate: "2023-01-01T00:00:00Z",
title: "Updated Title",
isDraft: false,
sourceRefName: "refs/heads/feature",
targetRefName: "refs/heads/main",
};
mockGitApi.updatePullRequest.mockResolvedValue(mockUpdatedPR);

const params = {
repositoryId: "repo123",
pullRequestId: 123,
title: "Updated Title",
// labels is undefined (not provided)
};

await handler(params);

// Verify that labels field is not included in the update request
const updateRequestArg = mockGitApi.updatePullRequest.mock.calls[0][0];
expect(updateRequestArg).not.toHaveProperty("labels");
expect(updateRequestArg).toEqual({
title: "Updated Title",
});
});

it("should remove all labels when empty array is provided", async () => {
configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider);

const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request);
if (!call) throw new Error("repo_update_pull_request tool not registered");
const [, , , handler] = call;

const mockUpdatedPR = {
pullRequestId: 123,
codeReviewId: 123,
repository: { name: "test-repo" },
status: PullRequestStatus.Active,
createdBy: {
displayName: "Test User",
uniqueName: "[email protected]",
},
creationDate: "2023-01-01T00:00:00Z",
title: "Test PR",
isDraft: false,
sourceRefName: "refs/heads/feature",
targetRefName: "refs/heads/main",
};
mockGitApi.updatePullRequest.mockResolvedValue(mockUpdatedPR);

const params = {
repositoryId: "repo123",
pullRequestId: 123,
labels: [],
};

await handler(params);

// Verify that labels field is included with empty array
expect(mockGitApi.updatePullRequest).toHaveBeenCalledWith(
{
labels: [],
},
"repo123",
123
);
});

it("should replace labels when array with values is provided", async () => {
configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider);

const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request);
if (!call) throw new Error("repo_update_pull_request tool not registered");
const [, , , handler] = call;

const mockUpdatedPR = {
pullRequestId: 123,
codeReviewId: 123,
repository: { name: "test-repo" },
status: PullRequestStatus.Active,
createdBy: {
displayName: "Test User",
uniqueName: "[email protected]",
},
creationDate: "2023-01-01T00:00:00Z",
title: "Test PR",
isDraft: false,
sourceRefName: "refs/heads/feature",
targetRefName: "refs/heads/main",
};
mockGitApi.updatePullRequest.mockResolvedValue(mockUpdatedPR);

const params = {
repositoryId: "repo123",
pullRequestId: 123,
labels: ["bug", "urgent"],
};

await handler(params);

// Verify that labels are mapped to WebApiTagDefinition format
expect(mockGitApi.updatePullRequest).toHaveBeenCalledWith(
{
labels: [{ name: "bug" }, { name: "urgent" }],
},
"repo123",
123
);
});
});

describe("repo_create_pull_request", () => {
Expand Down