@azure-devops/mcp 2.4.0 → 2.5.0-nightly.20260318

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, GitPullRequestMergeStrategy, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
3
+ import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, GitPullRequestMergeStrategy, VersionControlRecursionType, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
4
4
  import { z } from "zod";
5
5
  import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
6
6
  import { getEnumKeys } from "../utils.js";
@@ -23,6 +23,8 @@ const REPO_TOOLS = {
23
23
  update_pull_request_thread: "repo_update_pull_request_thread",
24
24
  search_commits: "repo_search_commits",
25
25
  list_pull_requests_by_commits: "repo_list_pull_requests_by_commits",
26
+ vote_pull_request: "repo_vote_pull_request",
27
+ list_directory: "repo_list_directory",
26
28
  };
27
29
  function branchesFilterOutIrrelevantProperties(branches, top) {
28
30
  return branches
@@ -103,18 +105,36 @@ function trimPullRequest(pr, includeDescription = false) {
103
105
  project: pr.repository?.project?.name,
104
106
  };
105
107
  }
108
+ // Helper function to build a version descriptor from branch or commit
109
+ function buildVersionDescriptor(version, versionType) {
110
+ if (!version) {
111
+ return undefined;
112
+ }
113
+ const versionTypeMap = {
114
+ Branch: GitVersionType.Branch,
115
+ Commit: GitVersionType.Commit,
116
+ Tag: GitVersionType.Tag,
117
+ };
118
+ return {
119
+ version: version,
120
+ versionType: versionTypeMap[versionType || "Branch"] ?? GitVersionType.Branch,
121
+ };
122
+ }
106
123
  function configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider) {
107
124
  server.tool(REPO_TOOLS.create_pull_request, "Create a new pull request.", {
108
- repositoryId: z.string().describe("The ID of the repository where the pull request will be created."),
125
+ repositoryId: z
126
+ .string()
127
+ .describe("The ID or name of the repository where the pull request will be created. When using a repository name instead of a GUID, the project parameter must also be provided."),
109
128
  sourceRefName: z.string().describe("The source branch name for the pull request, e.g., 'refs/heads/feature-branch'."),
110
129
  targetRefName: z.string().describe("The target branch name for the pull request, e.g., 'refs/heads/main'."),
111
130
  title: z.string().describe("The title of the pull request."),
112
131
  description: z.string().max(4000).optional().describe("The description of the pull request. Must not be longer than 4000 characters. Optional."),
113
132
  isDraft: z.boolean().optional().default(false).describe("Indicates whether the pull request is a draft. Defaults to false."),
133
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
114
134
  workItems: z.string().optional().describe("Work item IDs to associate with the pull request, space-separated."),
115
135
  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."),
116
136
  labels: z.array(z.string()).optional().describe("Array of label names to add to the pull request after creation."),
117
- }, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, workItems, forkSourceRepositoryId, labels }) => {
137
+ }, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, project, workItems, forkSourceRepositoryId, labels }) => {
118
138
  try {
119
139
  const connection = await connectionProvider();
120
140
  const gitApi = await connection.getGitApi();
@@ -127,7 +147,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
127
147
  }
128
148
  : undefined;
129
149
  const labelDefinitions = labels ? labels.map((label) => ({ name: label })) : undefined;
130
- const pullRequest = await gitApi.createPullRequest({
150
+ let pullRequest = await gitApi.createPullRequest({
131
151
  sourceRefName,
132
152
  targetRefName,
133
153
  title,
@@ -136,7 +156,19 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
136
156
  workItemRefs: workItemRefs,
137
157
  forkSource,
138
158
  labels: labelDefinitions,
139
- }, repositoryId);
159
+ supportsIterations: true,
160
+ }, repositoryId, project);
161
+ if (!pullRequest) {
162
+ const prs = await gitApi.getPullRequests(repositoryId, { sourceRefName, targetRefName, status: PullRequestStatus.Active }, project, undefined, 0, 1);
163
+ if (prs && prs.length > 0) {
164
+ pullRequest = prs[0];
165
+ }
166
+ else {
167
+ return {
168
+ content: [{ type: "text", text: "Pull request created but API returned no data." }],
169
+ };
170
+ }
171
+ }
140
172
  const trimmedPullRequest = trimPullRequest(pullRequest, true);
141
173
  return {
142
174
  content: [{ type: "text", text: JSON.stringify(trimmedPullRequest, null, 2) }],
@@ -151,11 +183,14 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
151
183
  }
152
184
  });
153
185
  server.tool(REPO_TOOLS.create_branch, "Create a new branch in the repository.", {
154
- repositoryId: z.string().describe("The ID of the repository where the branch will be created."),
186
+ repositoryId: z
187
+ .string()
188
+ .describe("The ID or name of the repository where the branch will be created. When using a repository name instead of a GUID, the project parameter must also be provided."),
155
189
  branchName: z.string().describe("The name of the new branch to create, e.g., 'feature-branch'."),
156
190
  sourceBranchName: z.string().optional().default("main").describe("The name of the source branch to create the new branch from. Defaults to 'main'."),
157
191
  sourceCommitId: z.string().optional().describe("The commit ID to create the branch from. If not provided, uses the latest commit of the source branch."),
158
- }, async ({ repositoryId, branchName, sourceBranchName, sourceCommitId }) => {
192
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
193
+ }, async ({ repositoryId, branchName, sourceBranchName, sourceCommitId, project }) => {
159
194
  try {
160
195
  const connection = await connectionProvider();
161
196
  const gitApi = await connection.getGitApi();
@@ -164,7 +199,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
164
199
  if (!commitId) {
165
200
  const sourceRefName = `refs/heads/${sourceBranchName}`;
166
201
  try {
167
- const sourceBranch = await gitApi.getRefs(repositoryId, undefined, "heads/", false, false, undefined, false, undefined, sourceBranchName);
202
+ const sourceBranch = await gitApi.getRefs(repositoryId, project, "heads/", false, false, undefined, false, undefined, sourceBranchName);
168
203
  const branch = sourceBranch.find((b) => b.name === sourceRefName);
169
204
  if (!branch || !branch.objectId) {
170
205
  return {
@@ -199,7 +234,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
199
234
  oldObjectId: "0000000000000000000000000000000000000000", // All zeros indicates creating a new ref
200
235
  };
201
236
  try {
202
- const result = await gitApi.updateRefs([refUpdate], repositoryId);
237
+ const result = await gitApi.updateRefs([refUpdate], repositoryId, project);
203
238
  // Check if the branch creation was successful
204
239
  if (result && result.length > 0 && result[0].success) {
205
240
  return {
@@ -245,8 +280,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
245
280
  }
246
281
  });
247
282
  server.tool(REPO_TOOLS.update_pull_request, "Update a Pull Request by ID with specified fields, including setting autocomplete with various completion options.", {
248
- repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
283
+ repositoryId: z.string().describe("The ID or name of the repository where the pull request exists. When using a repository name instead of a GUID, the project parameter must also be provided."),
249
284
  pullRequestId: z.number().describe("The ID of the pull request to update."),
285
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
250
286
  title: z.string().optional().describe("The new title for the pull request."),
251
287
  description: z.string().max(4000).optional().describe("The new description for the pull request. Must not be longer than 4000 characters."),
252
288
  isDraft: z.boolean().optional().describe("Whether the pull request should be a draft."),
@@ -261,7 +297,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
261
297
  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."),
262
298
  bypassReason: z.string().optional().describe("Reason for bypassing branch policies. When provided, branch policies will be automatically bypassed during autocompletion."),
263
299
  labels: z.array(z.string()).optional().describe("Array of label names to replace existing labels on the pull request. This will remove all current labels and add the specified ones."),
264
- }, async ({ repositoryId, pullRequestId, title, description, isDraft, targetRefName, status, autoComplete, mergeStrategy, deleteSourceBranch, transitionWorkItems, bypassReason, labels }) => {
300
+ }, async ({ repositoryId, pullRequestId, project, title, description, isDraft, targetRefName, status, autoComplete, mergeStrategy, deleteSourceBranch, transitionWorkItems, bypassReason, labels, }) => {
265
301
  try {
266
302
  const connection = await connectionProvider();
267
303
  const gitApi = await connection.getGitApi();
@@ -310,23 +346,23 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
310
346
  }
311
347
  // Update labels if provided
312
348
  if (labels) {
313
- const currentLabels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId);
349
+ const currentLabels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId, project);
314
350
  for (const currentLabel of currentLabels) {
315
351
  if (currentLabel.id) {
316
- await gitApi.deletePullRequestLabels(repositoryId, pullRequestId, currentLabel.id);
352
+ await gitApi.deletePullRequestLabels(repositoryId, pullRequestId, currentLabel.id, project);
317
353
  }
318
354
  }
319
355
  for (const label of labels) {
320
- await gitApi.createPullRequestLabel({ name: label }, repositoryId, pullRequestId);
356
+ await gitApi.createPullRequestLabel({ name: label }, repositoryId, pullRequestId, project);
321
357
  }
322
358
  }
323
359
  let updatedPullRequest;
324
360
  if (Object.keys(updateRequest).length > 0) {
325
- updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId);
361
+ updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId, project);
326
362
  }
327
363
  else {
328
364
  // If only labels were updated, get the current pull request
329
- updatedPullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId);
365
+ updatedPullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, project);
330
366
  }
331
367
  const trimmedUpdatedPullRequest = trimPullRequest(updatedPullRequest, true);
332
368
  return {
@@ -342,17 +378,18 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
342
378
  }
343
379
  });
344
380
  server.tool(REPO_TOOLS.update_pull_request_reviewers, "Add or remove reviewers for an existing pull request.", {
345
- repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
381
+ repositoryId: z.string().describe("The ID or name of the repository where the pull request exists. When using a repository name instead of a GUID, the project parameter must also be provided."),
346
382
  pullRequestId: z.number().describe("The ID of the pull request to update."),
347
383
  reviewerIds: z.array(z.string()).describe("List of reviewer ids to add or remove from the pull request."),
348
384
  action: z.enum(["add", "remove"]).describe("Action to perform on the reviewers. Can be 'add' or 'remove'."),
349
- }, async ({ repositoryId, pullRequestId, reviewerIds, action }) => {
385
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
386
+ }, async ({ repositoryId, pullRequestId, reviewerIds, action, project }) => {
350
387
  try {
351
388
  const connection = await connectionProvider();
352
389
  const gitApi = await connection.getGitApi();
353
390
  let updatedPullRequest;
354
391
  if (action === "add") {
355
- updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId);
392
+ updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId, project);
356
393
  const trimmedResponse = updatedPullRequest.map((item) => ({
357
394
  displayName: item.displayName,
358
395
  id: item.id,
@@ -367,7 +404,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
367
404
  }
368
405
  else {
369
406
  for (const reviewerId of reviewerIds) {
370
- await gitApi.deletePullRequestReviewer(repositoryId, pullRequestId, reviewerId);
407
+ await gitApi.deletePullRequestReviewer(repositoryId, pullRequestId, reviewerId, project);
371
408
  }
372
409
  return {
373
410
  content: [{ type: "text", text: `Reviewers with IDs ${reviewerIds.join(", ")} removed from pull request ${pullRequestId}.` }],
@@ -417,8 +454,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
417
454
  }
418
455
  });
419
456
  server.tool(REPO_TOOLS.list_pull_requests_by_repo_or_project, "Retrieve a list of pull requests for a given repository. Either repositoryId or project must be provided.", {
420
- repositoryId: z.string().optional().describe("The ID of the repository where the pull requests are located."),
421
- project: z.string().optional().describe("The ID of the project where the pull requests are located."),
457
+ repositoryId: z
458
+ .string()
459
+ .optional()
460
+ .describe("The ID or name of the repository where the pull requests are located. When using a repository name instead of a GUID, the project parameter must also be provided."),
461
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID, or to scope the search to a specific project."),
422
462
  top: z.number().default(100).describe("The maximum number of pull requests to return."),
423
463
  skip: z.number().default(0).describe("The number of pull requests to skip."),
424
464
  created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
@@ -543,9 +583,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
543
583
  }
544
584
  });
545
585
  server.tool(REPO_TOOLS.list_pull_request_threads, "Retrieve a list of comment threads for a pull request.", {
546
- repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
586
+ repositoryId: z
587
+ .string()
588
+ .describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
547
589
  pullRequestId: z.number().describe("The ID of the pull request for which to retrieve threads."),
548
- project: z.string().optional().describe("Project ID or project name (optional)"),
590
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
549
591
  iteration: z.number().optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
550
592
  baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
551
593
  top: z.number().default(100).describe("The maximum number of threads to return after filtering."),
@@ -601,10 +643,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
601
643
  }
602
644
  });
603
645
  server.tool(REPO_TOOLS.list_pull_request_thread_comments, "Retrieve a list of comments in a pull request thread.", {
604
- repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
646
+ repositoryId: z
647
+ .string()
648
+ .describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
605
649
  pullRequestId: z.number().describe("The ID of the pull request for which to retrieve thread comments."),
606
650
  threadId: z.number().describe("The ID of the thread for which to retrieve comments."),
607
- project: z.string().optional().describe("Project ID or project name (optional)"),
651
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
608
652
  top: z.number().default(100).describe("The maximum number of comments to return."),
609
653
  skip: z.number().default(0).describe("The number of comments to skip."),
610
654
  fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of trimmed data."),
@@ -635,14 +679,17 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
635
679
  }
636
680
  });
637
681
  server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
638
- repositoryId: z.string().describe("The ID of the repository where the branches are located."),
682
+ repositoryId: z
683
+ .string()
684
+ .describe("The ID or name of the repository where the branches are located. When using a repository name instead of a GUID, the project parameter must also be provided."),
639
685
  top: z.number().default(100).describe("The maximum number of branches to return. Defaults to 100."),
640
686
  filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
641
- }, async ({ repositoryId, top, filterContains }) => {
687
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
688
+ }, async ({ repositoryId, top, filterContains, project }) => {
642
689
  try {
643
690
  const connection = await connectionProvider();
644
691
  const gitApi = await connection.getGitApi();
645
- const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, undefined, undefined, undefined, filterContains);
692
+ const branches = await gitApi.getRefs(repositoryId, project, "heads/", undefined, undefined, undefined, undefined, undefined, filterContains);
646
693
  const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
647
694
  return {
648
695
  content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
@@ -657,14 +704,17 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
657
704
  }
658
705
  });
659
706
  server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branches for a given repository Id.", {
660
- repositoryId: z.string().describe("The ID of the repository where the branches are located."),
707
+ repositoryId: z
708
+ .string()
709
+ .describe("The ID or name of the repository where the branches are located. When using a repository name instead of a GUID, the project parameter must also be provided."),
661
710
  top: z.number().default(100).describe("The maximum number of branches to return."),
662
711
  filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
663
- }, async ({ repositoryId, top, filterContains }) => {
712
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
713
+ }, async ({ repositoryId, top, filterContains, project }) => {
664
714
  try {
665
715
  const connection = await connectionProvider();
666
716
  const gitApi = await connection.getGitApi();
667
- const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, true, undefined, undefined, filterContains);
717
+ const branches = await gitApi.getRefs(repositoryId, project, "heads/", undefined, undefined, true, undefined, undefined, filterContains);
668
718
  const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
669
719
  return {
670
720
  content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
@@ -706,13 +756,14 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
706
756
  }
707
757
  });
708
758
  server.tool(REPO_TOOLS.get_branch_by_name, "Get a branch by its name.", {
709
- repositoryId: z.string().describe("The ID of the repository where the branch is located."),
759
+ repositoryId: z.string().describe("The ID or name of the repository where the branch is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
710
760
  branchName: z.string().describe("The name of the branch to retrieve, e.g., 'main' or 'feature-branch'."),
711
- }, async ({ repositoryId, branchName }) => {
761
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
762
+ }, async ({ repositoryId, branchName, project }) => {
712
763
  try {
713
764
  const connection = await connectionProvider();
714
765
  const gitApi = await connection.getGitApi();
715
- const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", false, false, undefined, false, undefined, branchName);
766
+ const branches = await gitApi.getRefs(repositoryId, project, "heads/", false, false, undefined, false, undefined, branchName);
716
767
  const branch = branches.find((branch) => branch.name === `refs/heads/${branchName}` || branch.name === branchName);
717
768
  if (!branch) {
718
769
  return {
@@ -738,15 +789,18 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
738
789
  }
739
790
  });
740
791
  server.tool(REPO_TOOLS.get_pull_request_by_id, "Get a pull request by its ID.", {
741
- repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
792
+ repositoryId: z
793
+ .string()
794
+ .describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
742
795
  pullRequestId: z.number().describe("The ID of the pull request to retrieve."),
796
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
743
797
  includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to reference work items associated with the pull request."),
744
798
  includeLabels: z.boolean().optional().default(false).describe("Whether to include a summary of labels in the response."),
745
- }, async ({ repositoryId, pullRequestId, includeWorkItemRefs, includeLabels }) => {
799
+ }, async ({ repositoryId, pullRequestId, project, includeWorkItemRefs, includeLabels }) => {
746
800
  try {
747
801
  const connection = await connectionProvider();
748
802
  const gitApi = await connection.getGitApi();
749
- const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, undefined, undefined, undefined, undefined, undefined, includeWorkItemRefs);
803
+ const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, project, undefined, undefined, undefined, undefined, includeWorkItemRefs);
750
804
  if (includeLabels) {
751
805
  try {
752
806
  const projectId = pullRequest.repository?.project?.id;
@@ -789,11 +843,13 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
789
843
  }
790
844
  });
791
845
  server.tool(REPO_TOOLS.reply_to_comment, "Replies to a specific comment on a pull request.", {
792
- repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
846
+ repositoryId: z
847
+ .string()
848
+ .describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
793
849
  pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
794
850
  threadId: z.number().describe("The ID of the thread to which the comment will be added."),
795
851
  content: z.string().describe("The content of the comment to be added."),
796
- project: z.string().optional().describe("Project ID or project name (optional)"),
852
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
797
853
  fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of a simple confirmation message."),
798
854
  }, async ({ repositoryId, pullRequestId, threadId, content, project, fullResponse }) => {
799
855
  try {
@@ -825,10 +881,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
825
881
  }
826
882
  });
827
883
  server.tool(REPO_TOOLS.create_pull_request_thread, "Creates a new comment thread on a pull request.", {
828
- repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
884
+ repositoryId: z
885
+ .string()
886
+ .describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
829
887
  pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
830
888
  content: z.string().describe("The content of the comment to be added."),
831
- project: z.string().optional().describe("Project ID or project name (optional)"),
889
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
832
890
  filePath: z.string().optional().describe("The path of the file where the comment thread will be created. (optional)"),
833
891
  status: z
834
892
  .enum(getEnumKeys(CommentThreadStatus))
@@ -939,10 +997,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
939
997
  }
940
998
  });
941
999
  server.tool(REPO_TOOLS.update_pull_request_thread, "Updates an existing comment thread on a pull request.", {
942
- repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
1000
+ repositoryId: z
1001
+ .string()
1002
+ .describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
943
1003
  pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
944
1004
  threadId: z.number().describe("The ID of the thread to update."),
945
- project: z.string().optional().describe("Project ID or project name (optional)"),
1005
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
946
1006
  status: z
947
1007
  .enum(getEnumKeys(CommentThreadStatus))
948
1008
  .optional()
@@ -1149,5 +1209,101 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
1149
1209
  };
1150
1210
  }
1151
1211
  });
1212
+ server.tool(REPO_TOOLS.vote_pull_request, "Cast a vote on a pull request. Automatically adds the current user as a reviewer if they are not already one.", {
1213
+ repositoryId: z.string().describe("The ID or name of the repository. When using a repository name instead of a GUID, the project parameter must also be provided."),
1214
+ pullRequestId: z.number().describe("The ID of the pull request."),
1215
+ vote: z.enum(["Approved", "ApprovedWithSuggestions", "NoVote", "WaitingForAuthor", "Rejected"]).describe("The vote to cast: Approved(10), Suggestions(5), None(0), Waiting(-5), Rejected(-10)."),
1216
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
1217
+ }, async ({ repositoryId, pullRequestId, vote, project }) => {
1218
+ const connection = await connectionProvider();
1219
+ const gitApi = await connection.getGitApi();
1220
+ const userDetails = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
1221
+ const userId = userDetails.authenticatedUser.id;
1222
+ if (!userId) {
1223
+ throw new Error("Could not determine authenticated user ID.");
1224
+ }
1225
+ const voteMap = {
1226
+ Approved: 10,
1227
+ ApprovedWithSuggestions: 5,
1228
+ NoVote: 0,
1229
+ WaitingForAuthor: -5,
1230
+ Rejected: -10,
1231
+ };
1232
+ await gitApi.createPullRequestReviewer({ vote: voteMap[vote], id: userId }, repositoryId, pullRequestId, userId, project);
1233
+ return {
1234
+ content: [
1235
+ {
1236
+ type: "text",
1237
+ text: `Successfully cast vote '${vote}' on PR #${pullRequestId}.`,
1238
+ },
1239
+ ],
1240
+ };
1241
+ });
1242
+ server.tool(REPO_TOOLS.list_directory, "List files and folders in a directory within a repository. Useful for exploring the structure of a codebase or finding related files.", {
1243
+ repositoryId: z.string().describe("The ID or name of the repository."),
1244
+ path: z.string().optional().default("/").describe("The directory path to list (e.g., '/src' or '/src/components'). Defaults to repository root."),
1245
+ project: z.string().optional().describe("Project ID or name. Required if repositoryId is a name rather than a GUID."),
1246
+ version: z.string().optional().describe("The version identifier - branch name (e.g., 'main'), tag name, or commit SHA. Defaults to the repository's default branch."),
1247
+ versionType: z.enum(["Branch", "Commit", "Tag"]).optional().default("Branch").describe("The type of version identifier: 'Branch', 'Commit', or 'Tag'. Defaults to 'Branch'."),
1248
+ recursive: z.boolean().optional().default(false).describe("Whether to list items recursively. Defaults to false."),
1249
+ recursionDepth: z.number().optional().default(1).describe("Maximum depth for recursive listing (1-10). Only applies when recursive is true. Defaults to 1."),
1250
+ }, async ({ repositoryId, path, project, version, versionType, recursive, recursionDepth }) => {
1251
+ try {
1252
+ const connection = await connectionProvider();
1253
+ const gitApi = await connection.getGitApi();
1254
+ const versionDescriptor = buildVersionDescriptor(version, versionType);
1255
+ const clampedDepth = Math.min(Math.max(recursionDepth || 1, 1), 10);
1256
+ let recursionType = VersionControlRecursionType.OneLevel;
1257
+ if (recursive) {
1258
+ recursionType = VersionControlRecursionType.Full;
1259
+ }
1260
+ const items = await gitApi.getItems(repositoryId, project, path, recursionType, true, false, false, false, versionDescriptor);
1261
+ if (!items || items.length === 0) {
1262
+ return {
1263
+ content: [{ type: "text", text: `No items found at path: ${path}` }],
1264
+ };
1265
+ }
1266
+ let filteredItems = items;
1267
+ if (recursive && clampedDepth < 10) {
1268
+ const basePath = path === "/" ? "" : path;
1269
+ const baseDepth = basePath.split("/").filter((p) => p).length;
1270
+ filteredItems = items.filter((item) => {
1271
+ if (!item.path)
1272
+ return false;
1273
+ const itemDepth = item.path.split("/").filter((p) => p).length;
1274
+ return itemDepth <= baseDepth + clampedDepth;
1275
+ });
1276
+ }
1277
+ const formattedItems = filteredItems.map((item) => ({
1278
+ path: item.path,
1279
+ isFolder: item.isFolder,
1280
+ gitObjectType: item.gitObjectType,
1281
+ commitId: item.commitId,
1282
+ contentMetadata: item.contentMetadata
1283
+ ? {
1284
+ contentType: item.contentMetadata.contentType,
1285
+ fileName: item.contentMetadata.fileName,
1286
+ }
1287
+ : undefined,
1288
+ }));
1289
+ const response = {
1290
+ count: formattedItems.length,
1291
+ path: path,
1292
+ recursive: recursive,
1293
+ recursionDepth: recursive ? clampedDepth : undefined,
1294
+ items: formattedItems,
1295
+ };
1296
+ return {
1297
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
1298
+ };
1299
+ }
1300
+ catch (error) {
1301
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
1302
+ return {
1303
+ content: [{ type: "text", text: `Error listing directory: ${errorMessage}` }],
1304
+ isError: true,
1305
+ };
1306
+ }
1307
+ });
1152
1308
  }
1153
1309
  export { REPO_TOOLS, configureRepoTools };
@@ -76,29 +76,47 @@ function configureTestPlanTools(server, _, connectionProvider) {
76
76
  parentSuiteId: z.number().describe("ID of the parent suite under which the new suite will be created, if not given by user this can be id of a root suite of the test plan"),
77
77
  name: z.string().describe("Name of the child test suite"),
78
78
  }, async ({ project, planId, parentSuiteId, name }) => {
79
- try {
80
- const connection = await connectionProvider();
81
- const testPlanApi = await connection.getTestPlanApi();
82
- const testSuiteToCreate = {
83
- name,
84
- parentSuite: {
85
- id: parentSuiteId,
86
- name: "",
87
- },
88
- suiteType: 2,
89
- };
90
- const createdTestSuite = await testPlanApi.createTestSuite(testSuiteToCreate, project, planId);
91
- return {
92
- content: [{ type: "text", text: JSON.stringify(createdTestSuite, null, 2) }],
93
- };
94
- }
95
- catch (error) {
96
- const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
97
- return {
98
- content: [{ type: "text", text: `Error creating test suite: ${errorMessage}` }],
99
- isError: true,
100
- };
79
+ const maxRetries = 5;
80
+ const baseDelay = 500; // milliseconds
81
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
82
+ try {
83
+ const connection = await connectionProvider();
84
+ const testPlanApi = await connection.getTestPlanApi();
85
+ const testSuiteToCreate = {
86
+ name,
87
+ parentSuite: {
88
+ id: parentSuiteId,
89
+ name: "",
90
+ },
91
+ suiteType: 2,
92
+ };
93
+ const createdTestSuite = await testPlanApi.createTestSuite(testSuiteToCreate, project, planId);
94
+ return {
95
+ content: [{ type: "text", text: JSON.stringify(createdTestSuite, null, 2) }],
96
+ };
97
+ }
98
+ catch (error) {
99
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
100
+ // Check if it's a concurrency conflict error
101
+ const isConcurrencyError = errorMessage.includes("TF26071") || errorMessage.includes("got update") || errorMessage.includes("changed by someone else");
102
+ // If it's a concurrency error and we have retries left, wait and retry
103
+ if (isConcurrencyError && attempt < maxRetries) {
104
+ const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 200; // Exponential backoff with jitter
105
+ await new Promise((resolve) => setTimeout(resolve, delay));
106
+ continue; // Retry
107
+ }
108
+ // If not a concurrency error or out of retries, return error
109
+ return {
110
+ content: [{ type: "text", text: `Error creating test suite: ${errorMessage}` }],
111
+ isError: true,
112
+ };
113
+ }
101
114
  }
115
+ // This should never be reached, but TypeScript requires a return value
116
+ return {
117
+ content: [{ type: "text", text: "Error creating test suite: Maximum retries exceeded" }],
118
+ isError: true,
119
+ };
102
120
  });
103
121
  server.tool(Test_Plan_Tools.add_test_cases_to_suite, "Adds existing test cases to a test suite.", {
104
122
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
@@ -257,16 +275,48 @@ function configureTestPlanTools(server, _, connectionProvider) {
257
275
  };
258
276
  }
259
277
  });
260
- server.tool(Test_Plan_Tools.test_results_from_build_id, "Gets a list of test results for a given project and build ID.", {
278
+ server.tool(Test_Plan_Tools.test_results_from_build_id, "Gets a list of test results for a given project and build ID. Can filter by test outcome (e.g. Failed, Passed, Aborted). Returns test case titles, error messages, stack traces, and outcomes. Efficiently handles builds with large numbers of test runs.", {
261
279
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
262
280
  buildid: z.number().describe("The ID of the build."),
263
- }, async ({ project, buildid }) => {
281
+ outcomes: z.array(z.string()).optional().describe("Filter results by test outcome, e.g. ['Failed', 'Passed', 'Aborted']."),
282
+ }, async ({ project, buildid, outcomes }) => {
264
283
  try {
265
284
  const connection = await connectionProvider();
266
- const coreApi = await connection.getTestResultsApi();
267
- const testResults = await coreApi.getTestResultDetailsForBuild(project, buildid);
285
+ const testResultsApi = await connection.getTestResultsApi();
286
+ // Build filter expression for outcomes if specified
287
+ const outcomeFilter = outcomes?.map((o) => `Outcome eq '${o}'`).join(" or ");
288
+ // Fetch test result details for the build in a single API call
289
+ // This is more efficient than getTestRuns + getTestResults per run,
290
+ // especially for builds with many test runs (e.g., cloud testing with one run per test case)
291
+ const testResultDetails = await testResultsApi.getTestResultDetailsForBuild(project, buildid, undefined, // publishContext
292
+ undefined, // groupBy
293
+ outcomeFilter, // filter by outcome
294
+ undefined, // orderby
295
+ true // shouldIncludeResults - get individual test results, not just aggregates
296
+ );
297
+ // Extract individual test results from the grouped response
298
+ const allResults = [];
299
+ if (testResultDetails.resultsForGroup) {
300
+ for (const group of testResultDetails.resultsForGroup) {
301
+ if (group.results) {
302
+ allResults.push(...group.results);
303
+ }
304
+ }
305
+ }
306
+ // Format results to extract useful fields
307
+ const formattedResults = allResults.map((r) => ({
308
+ id: r.id,
309
+ testCaseTitle: r.testCaseTitle,
310
+ outcome: r.outcome,
311
+ errorMessage: r.errorMessage,
312
+ stackTrace: r.stackTrace,
313
+ automatedTestName: r.automatedTestName,
314
+ automatedTestStorage: r.automatedTestStorage,
315
+ durationInMs: r.durationInMs,
316
+ runId: r.testRun?.id,
317
+ }));
268
318
  return {
269
- content: [{ type: "text", text: JSON.stringify(testResults, null, 2) }],
319
+ content: [{ type: "text", text: JSON.stringify(formattedResults, null, 2) }],
270
320
  };
271
321
  }
272
322
  catch (error) {