@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.
- package/README.md +29 -8
- package/dist/auth.js +0 -0
- package/dist/index.js +0 -0
- package/dist/logger.js +0 -0
- package/dist/org-tenants.js +0 -0
- package/dist/prompts.js +0 -0
- package/dist/shared/elicitations.js +62 -0
- package/dist/tools/core.js +11 -3
- package/dist/tools/pipelines.js +67 -0
- package/dist/tools/repositories.js +200 -44
- package/dist/tools/test-plans.js +77 -27
- package/dist/tools/work-items.js +39 -0
- package/dist/tools/work.js +108 -15
- package/dist/tools.js +0 -0
- package/dist/useragent.js +0 -0
- package/dist/utils.js +0 -0
- package/dist/version.js +1 -1
- package/package.json +6 -7
- package/dist/tools/builds.js +0 -271
- package/dist/tools/releases.js +0 -97
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
421
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 };
|
package/dist/tools/test-plans.js
CHANGED
|
@@ -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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
name
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
|
267
|
-
|
|
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(
|
|
319
|
+
content: [{ type: "text", text: JSON.stringify(formattedResults, null, 2) }],
|
|
270
320
|
};
|
|
271
321
|
}
|
|
272
322
|
catch (error) {
|