From 60b7abf25b7bae0ef0e3b0dcd260b91a3c2fe393 Mon Sep 17 00:00:00 2001 From: Nikola Pejic Date: Thu, 25 Sep 2025 12:36:16 +0000 Subject: [PATCH 1/4] Initial change --- src/tools/repositories.ts | 96 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/tools/repositories.ts b/src/tools/repositories.ts index 1a643f8..c913086 100644 --- a/src/tools/repositories.ts +++ b/src/tools/repositories.ts @@ -33,6 +33,7 @@ const REPO_TOOLS = { get_branch_by_name: "repo_get_branch_by_name", get_pull_request_by_id: "repo_get_pull_request_by_id", create_pull_request: "repo_create_pull_request", + create_branch: "repo_create_branch", update_pull_request: "repo_update_pull_request", update_pull_request_reviewers: "repo_update_pull_request_reviewers", reply_to_comment: "repo_reply_to_comment", @@ -142,6 +143,101 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise { + const connection = await connectionProvider(); + const gitApi = await connection.getGitApi(); + + let commitId = sourceCommitId; + + // If no commit ID is provided, get the latest commit from the source branch + if (!commitId) { + const sourceRefName = `refs/heads/${sourceBranchName}`; + try { + const sourceBranch = await gitApi.getRefs(repositoryId, undefined, "heads/", false, false, undefined, false, undefined, sourceBranchName); + const branch = sourceBranch.find((b) => b.name === sourceRefName); + if (!branch || !branch.objectId) { + return { + content: [ + { + type: "text", + text: `Error: Source branch '${sourceBranchName}' not found in repository ${repositoryId}`, + }, + ], + isError: true, + }; + } + commitId = branch.objectId; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error retrieving source branch '${sourceBranchName}': ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + } + + // Create the new branch using updateRefs + const newRefName = `refs/heads/${branchName}`; + const refUpdate = { + name: newRefName, + newObjectId: commitId, + oldObjectId: "0000000000000000000000000000000000000000", // All zeros indicates creating a new ref + }; + + try { + const result = await gitApi.updateRefs([refUpdate], repositoryId); + + // Check if the branch creation was successful + if (result && result.length > 0 && result[0].success) { + return { + content: [ + { + type: "text", + text: `Branch '${branchName}' created successfully from '${sourceBranchName}' (${commitId})`, + }, + ], + }; + } else { + const errorMessage = result && result.length > 0 && result[0].customMessage + ? result[0].customMessage + : "Unknown error occurred during branch creation"; + return { + content: [ + { + type: "text", + text: `Error creating branch '${branchName}': ${errorMessage}`, + }, + ], + isError: true, + }; + } + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error creating branch '${branchName}': ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + } + ); + server.tool( REPO_TOOLS.update_pull_request, "Update a Pull Request by ID with specified fields.", From 5b0eff2da6badf98fc1736ca035002fdc4029f86 Mon Sep 17 00:00:00 2001 From: Nikola Pejic Date: Thu, 25 Sep 2025 12:37:15 +0000 Subject: [PATCH 2/4] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index baafc4e..50ed228 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ Interact with these Azure DevOps services: - **repo_get_branch_by_name**: Get a branch by its name. - **repo_get_pull_request_by_id**: Get a pull request by its ID. - **repo_create_pull_request**: Create a new pull request. +- **repo_create_branch**: Create a new branch in the repository. - **repo_update_pull_request_status**: Update the status of an existing pull request to active or abandoned. - **repo_update_pull_request**: Update various fields of an existing pull request (title, description, draft status, target branch). - **repo_update_pull_request_reviewers**: Add or remove reviewers for an existing pull request. From 28aeedd9f6c0606a68e1a3bb47e6a8be75b16d47 Mon Sep 17 00:00:00 2001 From: Nikola Pejic Date: Thu, 25 Sep 2025 12:43:20 +0000 Subject: [PATCH 3/4] Adding tests --- test/src/tools/repositories.test.ts | 307 ++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) diff --git a/test/src/tools/repositories.test.ts b/test/src/tools/repositories.test.ts index cfd0d06..92b232a 100644 --- a/test/src/tools/repositories.test.ts +++ b/test/src/tools/repositories.test.ts @@ -39,6 +39,7 @@ describe("repos tools", () => { updateThread: jest.MockedFunction<(...args: unknown[]) => Promise>; getCommits: jest.MockedFunction<(...args: unknown[]) => Promise>; getPullRequestQuery: jest.MockedFunction<(...args: unknown[]) => Promise>; + updateRefs: jest.MockedFunction<(...args: unknown[]) => Promise>; }; beforeEach(() => { @@ -64,6 +65,7 @@ describe("repos tools", () => { updateThread: jest.fn(), getCommits: jest.fn(), getPullRequestQuery: jest.fn(), + updateRefs: jest.fn(), }; connectionProvider = jest.fn().mockResolvedValue({ @@ -351,6 +353,311 @@ describe("repos tools", () => { }); }); + describe("repo_create_branch", () => { + it("should create branch with default source branch (main)", async () => { + configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); + + const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); + if (!call) throw new Error("repo_create_branch tool not registered"); + const [, , , handler] = call; + + const mockSourceBranch = [ + { + name: "refs/heads/main", + objectId: "abc123def456", + }, + ]; + const mockUpdateResult = [ + { + success: true, + updateStatus: 0, + }, + ]; + + mockGitApi.getRefs.mockResolvedValue(mockSourceBranch); + mockGitApi.updateRefs.mockResolvedValue(mockUpdateResult); + + const params = { + repositoryId: "repo123", + branchName: "feature-branch", + sourceBranchName: "main", + }; + + const result = await handler(params); + + expect(mockGitApi.getRefs).toHaveBeenCalledWith("repo123", undefined, "heads/", false, false, undefined, false, undefined, "main"); + expect(mockGitApi.updateRefs).toHaveBeenCalledWith( + [ + { + name: "refs/heads/feature-branch", + newObjectId: "abc123def456", + oldObjectId: "0000000000000000000000000000000000000000", + }, + ], + "repo123" + ); + + expect(result.content[0].text).toBe("Branch 'feature-branch' created successfully from 'main' (abc123def456)"); + }); + + it("should create branch with custom source branch", async () => { + configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); + + const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); + if (!call) throw new Error("repo_create_branch tool not registered"); + const [, , , handler] = call; + + const mockSourceBranch = [ + { + name: "refs/heads/develop", + objectId: "def456ghi789", + }, + ]; + const mockUpdateResult = [ + { + success: true, + updateStatus: 0, + }, + ]; + + mockGitApi.getRefs.mockResolvedValue(mockSourceBranch); + mockGitApi.updateRefs.mockResolvedValue(mockUpdateResult); + + const params = { + repositoryId: "repo123", + branchName: "feature-branch", + sourceBranchName: "develop", + }; + + const result = await handler(params); + + expect(mockGitApi.getRefs).toHaveBeenCalledWith("repo123", undefined, "heads/", false, false, undefined, false, undefined, "develop"); + expect(mockGitApi.updateRefs).toHaveBeenCalledWith( + [ + { + name: "refs/heads/feature-branch", + newObjectId: "def456ghi789", + oldObjectId: "0000000000000000000000000000000000000000", + }, + ], + "repo123" + ); + + expect(result.content[0].text).toBe("Branch 'feature-branch' created successfully from 'develop' (def456ghi789)"); + }); + + it("should create branch with specific commit ID", async () => { + configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); + + const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); + if (!call) throw new Error("repo_create_branch tool not registered"); + const [, , , handler] = call; + + const mockUpdateResult = [ + { + success: true, + updateStatus: 0, + }, + ]; + + mockGitApi.updateRefs.mockResolvedValue(mockUpdateResult); + + const params = { + repositoryId: "repo123", + branchName: "feature-branch", + sourceBranchName: "main", + sourceCommitId: "xyz789abc123", + }; + + const result = await handler(params); + + // Should not call getRefs when sourceCommitId is provided + expect(mockGitApi.getRefs).not.toHaveBeenCalled(); + expect(mockGitApi.updateRefs).toHaveBeenCalledWith( + [ + { + name: "refs/heads/feature-branch", + newObjectId: "xyz789abc123", + oldObjectId: "0000000000000000000000000000000000000000", + }, + ], + "repo123" + ); + + expect(result.content[0].text).toBe("Branch 'feature-branch' created successfully from 'main' (xyz789abc123)"); + }); + + it("should handle source branch not found error", async () => { + configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); + + const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); + if (!call) throw new Error("repo_create_branch tool not registered"); + const [, , , handler] = call; + + mockGitApi.getRefs.mockResolvedValue([]); + + const params = { + repositoryId: "repo123", + branchName: "feature-branch", + sourceBranchName: "nonexistent", + }; + + const result = await handler(params); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe("Error: Source branch 'nonexistent' not found in repository repo123"); + }); + + it("should handle getRefs API error", async () => { + configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); + + const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); + if (!call) throw new Error("repo_create_branch tool not registered"); + const [, , , handler] = call; + + const mockError = new Error("API Error"); + mockGitApi.getRefs.mockRejectedValue(mockError); + + const params = { + repositoryId: "repo123", + branchName: "feature-branch", + sourceBranchName: "main", + }; + + const result = await handler(params); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe("Error retrieving source branch 'main': API Error"); + }); + + it("should handle updateRefs failure", async () => { + configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); + + const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); + if (!call) throw new Error("repo_create_branch tool not registered"); + const [, , , handler] = call; + + const mockSourceBranch = [ + { + name: "refs/heads/main", + objectId: "abc123def456", + }, + ]; + const mockUpdateResult = [ + { + success: false, + customMessage: "Branch already exists", + }, + ]; + + mockGitApi.getRefs.mockResolvedValue(mockSourceBranch); + mockGitApi.updateRefs.mockResolvedValue(mockUpdateResult); + + const params = { + repositoryId: "repo123", + branchName: "existing-branch", + sourceBranchName: "main", + }; + + const result = await handler(params); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe("Error creating branch 'existing-branch': Branch already exists"); + }); + + it("should handle updateRefs failure without custom message", async () => { + configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); + + const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); + if (!call) throw new Error("repo_create_branch tool not registered"); + const [, , , handler] = call; + + const mockSourceBranch = [ + { + name: "refs/heads/main", + objectId: "abc123def456", + }, + ]; + const mockUpdateResult = [ + { + success: false, + }, + ]; + + mockGitApi.getRefs.mockResolvedValue(mockSourceBranch); + mockGitApi.updateRefs.mockResolvedValue(mockUpdateResult); + + const params = { + repositoryId: "repo123", + branchName: "failing-branch", + sourceBranchName: "main", + }; + + const result = await handler(params); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe("Error creating branch 'failing-branch': Unknown error occurred during branch creation"); + }); + + it("should handle updateRefs API error", async () => { + configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); + + const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); + if (!call) throw new Error("repo_create_branch tool not registered"); + const [, , , handler] = call; + + const mockSourceBranch = [ + { + name: "refs/heads/main", + objectId: "abc123def456", + }, + ]; + const mockError = new Error("Update API Error"); + + mockGitApi.getRefs.mockResolvedValue(mockSourceBranch); + mockGitApi.updateRefs.mockRejectedValue(mockError); + + const params = { + repositoryId: "repo123", + branchName: "feature-branch", + sourceBranchName: "main", + }; + + const result = await handler(params); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe("Error creating branch 'feature-branch': Update API Error"); + }); + + it("should handle source branch with missing objectId", async () => { + configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); + + const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); + if (!call) throw new Error("repo_create_branch tool not registered"); + const [, , , handler] = call; + + const mockSourceBranch = [ + { + name: "refs/heads/main", + // objectId is missing + }, + ]; + + mockGitApi.getRefs.mockResolvedValue(mockSourceBranch); + + const params = { + repositoryId: "repo123", + branchName: "feature-branch", + sourceBranchName: "main", + }; + + const result = await handler(params); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe("Error: Source branch 'main' not found in repository repo123"); + }); + }); + describe("repo_update_pull_request_reviewers", () => { it("should add reviewers to pull request", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); From cafd9476865ce206b23673d3f055e4d3387af310 Mon Sep 17 00:00:00 2001 From: Nikola Pejic Date: Thu, 25 Sep 2025 12:44:51 +0000 Subject: [PATCH 4/4] npm run format --- src/tools/repositories.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tools/repositories.ts b/src/tools/repositories.ts index c913086..6e56215 100644 --- a/src/tools/repositories.ts +++ b/src/tools/repositories.ts @@ -211,9 +211,7 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise 0 && result[0].customMessage - ? result[0].customMessage - : "Unknown error occurred during branch creation"; + const errorMessage = result && result.length > 0 && result[0].customMessage ? result[0].customMessage : "Unknown error occurred during branch creation"; return { content: [ {