@azure-devops/mcp 2.4.0-nightly.20260317 → 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.
@@ -122,16 +122,19 @@ function buildVersionDescriptor(version, versionType) {
122
122
  }
123
123
  function configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider) {
124
124
  server.tool(REPO_TOOLS.create_pull_request, "Create a new pull request.", {
125
- 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."),
126
128
  sourceRefName: z.string().describe("The source branch name for the pull request, e.g., 'refs/heads/feature-branch'."),
127
129
  targetRefName: z.string().describe("The target branch name for the pull request, e.g., 'refs/heads/main'."),
128
130
  title: z.string().describe("The title of the pull request."),
129
131
  description: z.string().max(4000).optional().describe("The description of the pull request. Must not be longer than 4000 characters. Optional."),
130
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."),
131
134
  workItems: z.string().optional().describe("Work item IDs to associate with the pull request, space-separated."),
132
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."),
133
136
  labels: z.array(z.string()).optional().describe("Array of label names to add to the pull request after creation."),
134
- }, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, workItems, forkSourceRepositoryId, labels }) => {
137
+ }, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, project, workItems, forkSourceRepositoryId, labels }) => {
135
138
  try {
136
139
  const connection = await connectionProvider();
137
140
  const gitApi = await connection.getGitApi();
@@ -154,9 +157,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
154
157
  forkSource,
155
158
  labels: labelDefinitions,
156
159
  supportsIterations: true,
157
- }, repositoryId);
160
+ }, repositoryId, project);
158
161
  if (!pullRequest) {
159
- const prs = await gitApi.getPullRequests(repositoryId, { sourceRefName, targetRefName, status: PullRequestStatus.Active }, undefined, undefined, 0, 1);
162
+ const prs = await gitApi.getPullRequests(repositoryId, { sourceRefName, targetRefName, status: PullRequestStatus.Active }, project, undefined, 0, 1);
160
163
  if (prs && prs.length > 0) {
161
164
  pullRequest = prs[0];
162
165
  }
@@ -180,11 +183,14 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
180
183
  }
181
184
  });
182
185
  server.tool(REPO_TOOLS.create_branch, "Create a new branch in the repository.", {
183
- 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."),
184
189
  branchName: z.string().describe("The name of the new branch to create, e.g., 'feature-branch'."),
185
190
  sourceBranchName: z.string().optional().default("main").describe("The name of the source branch to create the new branch from. Defaults to 'main'."),
186
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."),
187
- }, 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 }) => {
188
194
  try {
189
195
  const connection = await connectionProvider();
190
196
  const gitApi = await connection.getGitApi();
@@ -193,7 +199,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
193
199
  if (!commitId) {
194
200
  const sourceRefName = `refs/heads/${sourceBranchName}`;
195
201
  try {
196
- 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);
197
203
  const branch = sourceBranch.find((b) => b.name === sourceRefName);
198
204
  if (!branch || !branch.objectId) {
199
205
  return {
@@ -228,7 +234,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
228
234
  oldObjectId: "0000000000000000000000000000000000000000", // All zeros indicates creating a new ref
229
235
  };
230
236
  try {
231
- const result = await gitApi.updateRefs([refUpdate], repositoryId);
237
+ const result = await gitApi.updateRefs([refUpdate], repositoryId, project);
232
238
  // Check if the branch creation was successful
233
239
  if (result && result.length > 0 && result[0].success) {
234
240
  return {
@@ -274,8 +280,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
274
280
  }
275
281
  });
276
282
  server.tool(REPO_TOOLS.update_pull_request, "Update a Pull Request by ID with specified fields, including setting autocomplete with various completion options.", {
277
- 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."),
278
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."),
279
286
  title: z.string().optional().describe("The new title for the pull request."),
280
287
  description: z.string().max(4000).optional().describe("The new description for the pull request. Must not be longer than 4000 characters."),
281
288
  isDraft: z.boolean().optional().describe("Whether the pull request should be a draft."),
@@ -290,7 +297,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
290
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."),
291
298
  bypassReason: z.string().optional().describe("Reason for bypassing branch policies. When provided, branch policies will be automatically bypassed during autocompletion."),
292
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."),
293
- }, 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, }) => {
294
301
  try {
295
302
  const connection = await connectionProvider();
296
303
  const gitApi = await connection.getGitApi();
@@ -339,23 +346,23 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
339
346
  }
340
347
  // Update labels if provided
341
348
  if (labels) {
342
- const currentLabels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId);
349
+ const currentLabels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId, project);
343
350
  for (const currentLabel of currentLabels) {
344
351
  if (currentLabel.id) {
345
- await gitApi.deletePullRequestLabels(repositoryId, pullRequestId, currentLabel.id);
352
+ await gitApi.deletePullRequestLabels(repositoryId, pullRequestId, currentLabel.id, project);
346
353
  }
347
354
  }
348
355
  for (const label of labels) {
349
- await gitApi.createPullRequestLabel({ name: label }, repositoryId, pullRequestId);
356
+ await gitApi.createPullRequestLabel({ name: label }, repositoryId, pullRequestId, project);
350
357
  }
351
358
  }
352
359
  let updatedPullRequest;
353
360
  if (Object.keys(updateRequest).length > 0) {
354
- updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId);
361
+ updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId, project);
355
362
  }
356
363
  else {
357
364
  // If only labels were updated, get the current pull request
358
- updatedPullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId);
365
+ updatedPullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, project);
359
366
  }
360
367
  const trimmedUpdatedPullRequest = trimPullRequest(updatedPullRequest, true);
361
368
  return {
@@ -371,17 +378,18 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
371
378
  }
372
379
  });
373
380
  server.tool(REPO_TOOLS.update_pull_request_reviewers, "Add or remove reviewers for an existing pull request.", {
374
- 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."),
375
382
  pullRequestId: z.number().describe("The ID of the pull request to update."),
376
383
  reviewerIds: z.array(z.string()).describe("List of reviewer ids to add or remove from the pull request."),
377
384
  action: z.enum(["add", "remove"]).describe("Action to perform on the reviewers. Can be 'add' or 'remove'."),
378
- }, 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 }) => {
379
387
  try {
380
388
  const connection = await connectionProvider();
381
389
  const gitApi = await connection.getGitApi();
382
390
  let updatedPullRequest;
383
391
  if (action === "add") {
384
- updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId);
392
+ updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId, project);
385
393
  const trimmedResponse = updatedPullRequest.map((item) => ({
386
394
  displayName: item.displayName,
387
395
  id: item.id,
@@ -396,7 +404,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
396
404
  }
397
405
  else {
398
406
  for (const reviewerId of reviewerIds) {
399
- await gitApi.deletePullRequestReviewer(repositoryId, pullRequestId, reviewerId);
407
+ await gitApi.deletePullRequestReviewer(repositoryId, pullRequestId, reviewerId, project);
400
408
  }
401
409
  return {
402
410
  content: [{ type: "text", text: `Reviewers with IDs ${reviewerIds.join(", ")} removed from pull request ${pullRequestId}.` }],
@@ -446,8 +454,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
446
454
  }
447
455
  });
448
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.", {
449
- repositoryId: z.string().optional().describe("The ID of the repository where the pull requests are located."),
450
- 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."),
451
462
  top: z.number().default(100).describe("The maximum number of pull requests to return."),
452
463
  skip: z.number().default(0).describe("The number of pull requests to skip."),
453
464
  created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
@@ -572,9 +583,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
572
583
  }
573
584
  });
574
585
  server.tool(REPO_TOOLS.list_pull_request_threads, "Retrieve a list of comment threads for a pull request.", {
575
- 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."),
576
589
  pullRequestId: z.number().describe("The ID of the pull request for which to retrieve threads."),
577
- 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."),
578
591
  iteration: z.number().optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
579
592
  baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
580
593
  top: z.number().default(100).describe("The maximum number of threads to return after filtering."),
@@ -630,10 +643,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
630
643
  }
631
644
  });
632
645
  server.tool(REPO_TOOLS.list_pull_request_thread_comments, "Retrieve a list of comments in a pull request thread.", {
633
- 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."),
634
649
  pullRequestId: z.number().describe("The ID of the pull request for which to retrieve thread comments."),
635
650
  threadId: z.number().describe("The ID of the thread for which to retrieve comments."),
636
- 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."),
637
652
  top: z.number().default(100).describe("The maximum number of comments to return."),
638
653
  skip: z.number().default(0).describe("The number of comments to skip."),
639
654
  fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of trimmed data."),
@@ -664,14 +679,17 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
664
679
  }
665
680
  });
666
681
  server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
667
- 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."),
668
685
  top: z.number().default(100).describe("The maximum number of branches to return. Defaults to 100."),
669
686
  filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
670
- }, 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 }) => {
671
689
  try {
672
690
  const connection = await connectionProvider();
673
691
  const gitApi = await connection.getGitApi();
674
- 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);
675
693
  const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
676
694
  return {
677
695
  content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
@@ -686,14 +704,17 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
686
704
  }
687
705
  });
688
706
  server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branches for a given repository Id.", {
689
- 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."),
690
710
  top: z.number().default(100).describe("The maximum number of branches to return."),
691
711
  filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
692
- }, 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 }) => {
693
714
  try {
694
715
  const connection = await connectionProvider();
695
716
  const gitApi = await connection.getGitApi();
696
- 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);
697
718
  const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
698
719
  return {
699
720
  content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
@@ -735,13 +756,14 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
735
756
  }
736
757
  });
737
758
  server.tool(REPO_TOOLS.get_branch_by_name, "Get a branch by its name.", {
738
- 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."),
739
760
  branchName: z.string().describe("The name of the branch to retrieve, e.g., 'main' or 'feature-branch'."),
740
- }, 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 }) => {
741
763
  try {
742
764
  const connection = await connectionProvider();
743
765
  const gitApi = await connection.getGitApi();
744
- 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);
745
767
  const branch = branches.find((branch) => branch.name === `refs/heads/${branchName}` || branch.name === branchName);
746
768
  if (!branch) {
747
769
  return {
@@ -821,11 +843,13 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
821
843
  }
822
844
  });
823
845
  server.tool(REPO_TOOLS.reply_to_comment, "Replies to a specific comment on a pull request.", {
824
- 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."),
825
849
  pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
826
850
  threadId: z.number().describe("The ID of the thread to which the comment will be added."),
827
851
  content: z.string().describe("The content of the comment to be added."),
828
- 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."),
829
853
  fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of a simple confirmation message."),
830
854
  }, async ({ repositoryId, pullRequestId, threadId, content, project, fullResponse }) => {
831
855
  try {
@@ -857,10 +881,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
857
881
  }
858
882
  });
859
883
  server.tool(REPO_TOOLS.create_pull_request_thread, "Creates a new comment thread on a pull request.", {
860
- 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."),
861
887
  pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
862
888
  content: z.string().describe("The content of the comment to be added."),
863
- 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."),
864
890
  filePath: z.string().optional().describe("The path of the file where the comment thread will be created. (optional)"),
865
891
  status: z
866
892
  .enum(getEnumKeys(CommentThreadStatus))
@@ -971,10 +997,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
971
997
  }
972
998
  });
973
999
  server.tool(REPO_TOOLS.update_pull_request_thread, "Updates an existing comment thread on a pull request.", {
974
- 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."),
975
1003
  pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
976
1004
  threadId: z.number().describe("The ID of the thread to update."),
977
- 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."),
978
1006
  status: z
979
1007
  .enum(getEnumKeys(CommentThreadStatus))
980
1008
  .optional()
@@ -1182,10 +1210,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
1182
1210
  }
1183
1211
  });
1184
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.", {
1185
- repositoryId: z.string().describe("The ID of the repository."),
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."),
1186
1214
  pullRequestId: z.number().describe("The ID of the pull request."),
1187
1215
  vote: z.enum(["Approved", "ApprovedWithSuggestions", "NoVote", "WaitingForAuthor", "Rejected"]).describe("The vote to cast: Approved(10), Suggestions(5), None(0), Waiting(-5), Rejected(-10)."),
1188
- }, async ({ repositoryId, pullRequestId, vote }) => {
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 }) => {
1189
1218
  const connection = await connectionProvider();
1190
1219
  const gitApi = await connection.getGitApi();
1191
1220
  const userDetails = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
@@ -1200,7 +1229,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
1200
1229
  WaitingForAuthor: -5,
1201
1230
  Rejected: -10,
1202
1231
  };
1203
- await gitApi.createPullRequestReviewer({ vote: voteMap[vote], id: userId }, repositoryId, pullRequestId, userId);
1232
+ await gitApi.createPullRequestReviewer({ vote: voteMap[vote], id: userId }, repositoryId, pullRequestId, userId, project);
1204
1233
  return {
1205
1234
  content: [
1206
1235
  {
@@ -275,16 +275,48 @@ function configureTestPlanTools(server, _, connectionProvider) {
275
275
  };
276
276
  }
277
277
  });
278
- 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.", {
279
279
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
280
280
  buildid: z.number().describe("The ID of the build."),
281
- }, 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 }) => {
282
283
  try {
283
284
  const connection = await connectionProvider();
284
- const coreApi = await connection.getTestResultsApi();
285
- 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
+ }));
286
318
  return {
287
- content: [{ type: "text", text: JSON.stringify(testResults, null, 2) }],
319
+ content: [{ type: "text", text: JSON.stringify(formattedResults, null, 2) }],
288
320
  };
289
321
  }
290
322
  catch (error) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.4.0-nightly.20260317";
1
+ export const packageVersion = "2.5.0-nightly.20260318";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.4.0-nightly.20260317",
3
+ "version": "2.5.0-nightly.20260318",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",