@azure-devops/mcp 2.4.0 → 2.5.0-nightly.20260319
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 +213 -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
|
|
@@ -84,6 +86,9 @@ function filterReposByName(repositories, repoNameFilter) {
|
|
|
84
86
|
return filteredByName;
|
|
85
87
|
}
|
|
86
88
|
function trimPullRequest(pr, includeDescription = false) {
|
|
89
|
+
if (!pr) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
87
92
|
return {
|
|
88
93
|
pullRequestId: pr.pullRequestId,
|
|
89
94
|
codeReviewId: pr.codeReviewId,
|
|
@@ -103,18 +108,36 @@ function trimPullRequest(pr, includeDescription = false) {
|
|
|
103
108
|
project: pr.repository?.project?.name,
|
|
104
109
|
};
|
|
105
110
|
}
|
|
111
|
+
// Helper function to build a version descriptor from branch or commit
|
|
112
|
+
function buildVersionDescriptor(version, versionType) {
|
|
113
|
+
if (!version) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const versionTypeMap = {
|
|
117
|
+
Branch: GitVersionType.Branch,
|
|
118
|
+
Commit: GitVersionType.Commit,
|
|
119
|
+
Tag: GitVersionType.Tag,
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
version: version,
|
|
123
|
+
versionType: versionTypeMap[versionType || "Branch"] ?? GitVersionType.Branch,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
106
126
|
function configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
107
127
|
server.tool(REPO_TOOLS.create_pull_request, "Create a new pull request.", {
|
|
108
|
-
repositoryId: z
|
|
128
|
+
repositoryId: z
|
|
129
|
+
.string()
|
|
130
|
+
.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
131
|
sourceRefName: z.string().describe("The source branch name for the pull request, e.g., 'refs/heads/feature-branch'."),
|
|
110
132
|
targetRefName: z.string().describe("The target branch name for the pull request, e.g., 'refs/heads/main'."),
|
|
111
133
|
title: z.string().describe("The title of the pull request."),
|
|
112
134
|
description: z.string().max(4000).optional().describe("The description of the pull request. Must not be longer than 4000 characters. Optional."),
|
|
113
135
|
isDraft: z.boolean().optional().default(false).describe("Indicates whether the pull request is a draft. Defaults to false."),
|
|
136
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
114
137
|
workItems: z.string().optional().describe("Work item IDs to associate with the pull request, space-separated."),
|
|
115
138
|
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
139
|
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 }) => {
|
|
140
|
+
}, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, project, workItems, forkSourceRepositoryId, labels }) => {
|
|
118
141
|
try {
|
|
119
142
|
const connection = await connectionProvider();
|
|
120
143
|
const gitApi = await connection.getGitApi();
|
|
@@ -127,7 +150,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
127
150
|
}
|
|
128
151
|
: undefined;
|
|
129
152
|
const labelDefinitions = labels ? labels.map((label) => ({ name: label })) : undefined;
|
|
130
|
-
|
|
153
|
+
let pullRequest = await gitApi.createPullRequest({
|
|
131
154
|
sourceRefName,
|
|
132
155
|
targetRefName,
|
|
133
156
|
title,
|
|
@@ -136,8 +159,25 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
136
159
|
workItemRefs: workItemRefs,
|
|
137
160
|
forkSource,
|
|
138
161
|
labels: labelDefinitions,
|
|
139
|
-
|
|
162
|
+
supportsIterations: true,
|
|
163
|
+
}, repositoryId, project);
|
|
164
|
+
if (!pullRequest) {
|
|
165
|
+
const prs = await gitApi.getPullRequests(repositoryId, { sourceRefName, targetRefName, status: PullRequestStatus.Active }, project, undefined, 0, 1);
|
|
166
|
+
if (prs && prs.length > 0) {
|
|
167
|
+
pullRequest = prs[0];
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: "text", text: "Pull request created but API returned no data." }],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
140
175
|
const trimmedPullRequest = trimPullRequest(pullRequest, true);
|
|
176
|
+
if (!trimmedPullRequest) {
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text", text: "Pull request created but API returned no data." }],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
141
181
|
return {
|
|
142
182
|
content: [{ type: "text", text: JSON.stringify(trimmedPullRequest, null, 2) }],
|
|
143
183
|
};
|
|
@@ -151,11 +191,14 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
151
191
|
}
|
|
152
192
|
});
|
|
153
193
|
server.tool(REPO_TOOLS.create_branch, "Create a new branch in the repository.", {
|
|
154
|
-
repositoryId: z
|
|
194
|
+
repositoryId: z
|
|
195
|
+
.string()
|
|
196
|
+
.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
197
|
branchName: z.string().describe("The name of the new branch to create, e.g., 'feature-branch'."),
|
|
156
198
|
sourceBranchName: z.string().optional().default("main").describe("The name of the source branch to create the new branch from. Defaults to 'main'."),
|
|
157
199
|
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
|
-
|
|
200
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
201
|
+
}, async ({ repositoryId, branchName, sourceBranchName, sourceCommitId, project }) => {
|
|
159
202
|
try {
|
|
160
203
|
const connection = await connectionProvider();
|
|
161
204
|
const gitApi = await connection.getGitApi();
|
|
@@ -164,7 +207,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
164
207
|
if (!commitId) {
|
|
165
208
|
const sourceRefName = `refs/heads/${sourceBranchName}`;
|
|
166
209
|
try {
|
|
167
|
-
const sourceBranch = await gitApi.getRefs(repositoryId,
|
|
210
|
+
const sourceBranch = await gitApi.getRefs(repositoryId, project, "heads/", false, false, undefined, false, undefined, sourceBranchName);
|
|
168
211
|
const branch = sourceBranch.find((b) => b.name === sourceRefName);
|
|
169
212
|
if (!branch || !branch.objectId) {
|
|
170
213
|
return {
|
|
@@ -199,7 +242,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
199
242
|
oldObjectId: "0000000000000000000000000000000000000000", // All zeros indicates creating a new ref
|
|
200
243
|
};
|
|
201
244
|
try {
|
|
202
|
-
const result = await gitApi.updateRefs([refUpdate], repositoryId);
|
|
245
|
+
const result = await gitApi.updateRefs([refUpdate], repositoryId, project);
|
|
203
246
|
// Check if the branch creation was successful
|
|
204
247
|
if (result && result.length > 0 && result[0].success) {
|
|
205
248
|
return {
|
|
@@ -245,8 +288,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
245
288
|
}
|
|
246
289
|
});
|
|
247
290
|
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."),
|
|
291
|
+
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
292
|
pullRequestId: z.number().describe("The ID of the pull request to update."),
|
|
293
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
250
294
|
title: z.string().optional().describe("The new title for the pull request."),
|
|
251
295
|
description: z.string().max(4000).optional().describe("The new description for the pull request. Must not be longer than 4000 characters."),
|
|
252
296
|
isDraft: z.boolean().optional().describe("Whether the pull request should be a draft."),
|
|
@@ -261,7 +305,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
261
305
|
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
306
|
bypassReason: z.string().optional().describe("Reason for bypassing branch policies. When provided, branch policies will be automatically bypassed during autocompletion."),
|
|
263
307
|
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 }) => {
|
|
308
|
+
}, async ({ repositoryId, pullRequestId, project, title, description, isDraft, targetRefName, status, autoComplete, mergeStrategy, deleteSourceBranch, transitionWorkItems, bypassReason, labels, }) => {
|
|
265
309
|
try {
|
|
266
310
|
const connection = await connectionProvider();
|
|
267
311
|
const gitApi = await connection.getGitApi();
|
|
@@ -310,25 +354,30 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
310
354
|
}
|
|
311
355
|
// Update labels if provided
|
|
312
356
|
if (labels) {
|
|
313
|
-
const currentLabels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId);
|
|
357
|
+
const currentLabels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId, project);
|
|
314
358
|
for (const currentLabel of currentLabels) {
|
|
315
359
|
if (currentLabel.id) {
|
|
316
|
-
await gitApi.deletePullRequestLabels(repositoryId, pullRequestId, currentLabel.id);
|
|
360
|
+
await gitApi.deletePullRequestLabels(repositoryId, pullRequestId, currentLabel.id, project);
|
|
317
361
|
}
|
|
318
362
|
}
|
|
319
363
|
for (const label of labels) {
|
|
320
|
-
await gitApi.createPullRequestLabel({ name: label }, repositoryId, pullRequestId);
|
|
364
|
+
await gitApi.createPullRequestLabel({ name: label }, repositoryId, pullRequestId, project);
|
|
321
365
|
}
|
|
322
366
|
}
|
|
323
367
|
let updatedPullRequest;
|
|
324
368
|
if (Object.keys(updateRequest).length > 0) {
|
|
325
|
-
updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId);
|
|
369
|
+
updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId, project);
|
|
326
370
|
}
|
|
327
371
|
else {
|
|
328
372
|
// If only labels were updated, get the current pull request
|
|
329
|
-
updatedPullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId);
|
|
373
|
+
updatedPullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, project);
|
|
330
374
|
}
|
|
331
375
|
const trimmedUpdatedPullRequest = trimPullRequest(updatedPullRequest, true);
|
|
376
|
+
if (!trimmedUpdatedPullRequest) {
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: "text", text: "Pull request updated but API returned no data." }],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
332
381
|
return {
|
|
333
382
|
content: [{ type: "text", text: JSON.stringify(trimmedUpdatedPullRequest, null, 2) }],
|
|
334
383
|
};
|
|
@@ -342,17 +391,18 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
342
391
|
}
|
|
343
392
|
});
|
|
344
393
|
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."),
|
|
394
|
+
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
395
|
pullRequestId: z.number().describe("The ID of the pull request to update."),
|
|
347
396
|
reviewerIds: z.array(z.string()).describe("List of reviewer ids to add or remove from the pull request."),
|
|
348
397
|
action: z.enum(["add", "remove"]).describe("Action to perform on the reviewers. Can be 'add' or 'remove'."),
|
|
349
|
-
|
|
398
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
399
|
+
}, async ({ repositoryId, pullRequestId, reviewerIds, action, project }) => {
|
|
350
400
|
try {
|
|
351
401
|
const connection = await connectionProvider();
|
|
352
402
|
const gitApi = await connection.getGitApi();
|
|
353
403
|
let updatedPullRequest;
|
|
354
404
|
if (action === "add") {
|
|
355
|
-
updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId);
|
|
405
|
+
updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId, project);
|
|
356
406
|
const trimmedResponse = updatedPullRequest.map((item) => ({
|
|
357
407
|
displayName: item.displayName,
|
|
358
408
|
id: item.id,
|
|
@@ -367,7 +417,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
367
417
|
}
|
|
368
418
|
else {
|
|
369
419
|
for (const reviewerId of reviewerIds) {
|
|
370
|
-
await gitApi.deletePullRequestReviewer(repositoryId, pullRequestId, reviewerId);
|
|
420
|
+
await gitApi.deletePullRequestReviewer(repositoryId, pullRequestId, reviewerId, project);
|
|
371
421
|
}
|
|
372
422
|
return {
|
|
373
423
|
content: [{ type: "text", text: `Reviewers with IDs ${reviewerIds.join(", ")} removed from pull request ${pullRequestId}.` }],
|
|
@@ -417,8 +467,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
417
467
|
}
|
|
418
468
|
});
|
|
419
469
|
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
|
-
|
|
470
|
+
repositoryId: z
|
|
471
|
+
.string()
|
|
472
|
+
.optional()
|
|
473
|
+
.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."),
|
|
474
|
+
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
475
|
top: z.number().default(100).describe("The maximum number of pull requests to return."),
|
|
423
476
|
skip: z.number().default(0).describe("The number of pull requests to skip."),
|
|
424
477
|
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
|
|
@@ -543,9 +596,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
543
596
|
}
|
|
544
597
|
});
|
|
545
598
|
server.tool(REPO_TOOLS.list_pull_request_threads, "Retrieve a list of comment threads for a pull request.", {
|
|
546
|
-
repositoryId: z
|
|
599
|
+
repositoryId: z
|
|
600
|
+
.string()
|
|
601
|
+
.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
602
|
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
|
|
603
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
549
604
|
iteration: z.number().optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
|
|
550
605
|
baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
|
|
551
606
|
top: z.number().default(100).describe("The maximum number of threads to return after filtering."),
|
|
@@ -601,10 +656,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
601
656
|
}
|
|
602
657
|
});
|
|
603
658
|
server.tool(REPO_TOOLS.list_pull_request_thread_comments, "Retrieve a list of comments in a pull request thread.", {
|
|
604
|
-
repositoryId: z
|
|
659
|
+
repositoryId: z
|
|
660
|
+
.string()
|
|
661
|
+
.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
662
|
pullRequestId: z.number().describe("The ID of the pull request for which to retrieve thread comments."),
|
|
606
663
|
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
|
|
664
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
608
665
|
top: z.number().default(100).describe("The maximum number of comments to return."),
|
|
609
666
|
skip: z.number().default(0).describe("The number of comments to skip."),
|
|
610
667
|
fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of trimmed data."),
|
|
@@ -635,14 +692,17 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
635
692
|
}
|
|
636
693
|
});
|
|
637
694
|
server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
|
|
638
|
-
repositoryId: z
|
|
695
|
+
repositoryId: z
|
|
696
|
+
.string()
|
|
697
|
+
.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
698
|
top: z.number().default(100).describe("The maximum number of branches to return. Defaults to 100."),
|
|
640
699
|
filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
|
|
641
|
-
|
|
700
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
701
|
+
}, async ({ repositoryId, top, filterContains, project }) => {
|
|
642
702
|
try {
|
|
643
703
|
const connection = await connectionProvider();
|
|
644
704
|
const gitApi = await connection.getGitApi();
|
|
645
|
-
const branches = await gitApi.getRefs(repositoryId,
|
|
705
|
+
const branches = await gitApi.getRefs(repositoryId, project, "heads/", undefined, undefined, undefined, undefined, undefined, filterContains);
|
|
646
706
|
const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
|
|
647
707
|
return {
|
|
648
708
|
content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
|
|
@@ -657,14 +717,17 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
657
717
|
}
|
|
658
718
|
});
|
|
659
719
|
server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branches for a given repository Id.", {
|
|
660
|
-
repositoryId: z
|
|
720
|
+
repositoryId: z
|
|
721
|
+
.string()
|
|
722
|
+
.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
723
|
top: z.number().default(100).describe("The maximum number of branches to return."),
|
|
662
724
|
filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
|
|
663
|
-
|
|
725
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
726
|
+
}, async ({ repositoryId, top, filterContains, project }) => {
|
|
664
727
|
try {
|
|
665
728
|
const connection = await connectionProvider();
|
|
666
729
|
const gitApi = await connection.getGitApi();
|
|
667
|
-
const branches = await gitApi.getRefs(repositoryId,
|
|
730
|
+
const branches = await gitApi.getRefs(repositoryId, project, "heads/", undefined, undefined, true, undefined, undefined, filterContains);
|
|
668
731
|
const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
|
|
669
732
|
return {
|
|
670
733
|
content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
|
|
@@ -706,13 +769,14 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
706
769
|
}
|
|
707
770
|
});
|
|
708
771
|
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."),
|
|
772
|
+
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
773
|
branchName: z.string().describe("The name of the branch to retrieve, e.g., 'main' or 'feature-branch'."),
|
|
711
|
-
|
|
774
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
775
|
+
}, async ({ repositoryId, branchName, project }) => {
|
|
712
776
|
try {
|
|
713
777
|
const connection = await connectionProvider();
|
|
714
778
|
const gitApi = await connection.getGitApi();
|
|
715
|
-
const branches = await gitApi.getRefs(repositoryId,
|
|
779
|
+
const branches = await gitApi.getRefs(repositoryId, project, "heads/", false, false, undefined, false, undefined, branchName);
|
|
716
780
|
const branch = branches.find((branch) => branch.name === `refs/heads/${branchName}` || branch.name === branchName);
|
|
717
781
|
if (!branch) {
|
|
718
782
|
return {
|
|
@@ -738,15 +802,18 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
738
802
|
}
|
|
739
803
|
});
|
|
740
804
|
server.tool(REPO_TOOLS.get_pull_request_by_id, "Get a pull request by its ID.", {
|
|
741
|
-
repositoryId: z
|
|
805
|
+
repositoryId: z
|
|
806
|
+
.string()
|
|
807
|
+
.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
808
|
pullRequestId: z.number().describe("The ID of the pull request to retrieve."),
|
|
809
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
743
810
|
includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to reference work items associated with the pull request."),
|
|
744
811
|
includeLabels: z.boolean().optional().default(false).describe("Whether to include a summary of labels in the response."),
|
|
745
|
-
}, async ({ repositoryId, pullRequestId, includeWorkItemRefs, includeLabels }) => {
|
|
812
|
+
}, async ({ repositoryId, pullRequestId, project, includeWorkItemRefs, includeLabels }) => {
|
|
746
813
|
try {
|
|
747
814
|
const connection = await connectionProvider();
|
|
748
815
|
const gitApi = await connection.getGitApi();
|
|
749
|
-
const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId,
|
|
816
|
+
const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, project, undefined, undefined, undefined, undefined, includeWorkItemRefs);
|
|
750
817
|
if (includeLabels) {
|
|
751
818
|
try {
|
|
752
819
|
const projectId = pullRequest.repository?.project?.id;
|
|
@@ -789,11 +856,13 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
789
856
|
}
|
|
790
857
|
});
|
|
791
858
|
server.tool(REPO_TOOLS.reply_to_comment, "Replies to a specific comment on a pull request.", {
|
|
792
|
-
repositoryId: z
|
|
859
|
+
repositoryId: z
|
|
860
|
+
.string()
|
|
861
|
+
.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
862
|
pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
|
|
794
863
|
threadId: z.number().describe("The ID of the thread to which the comment will be added."),
|
|
795
864
|
content: z.string().describe("The content of the comment to be added."),
|
|
796
|
-
project: z.string().optional().describe("Project ID or project name
|
|
865
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
797
866
|
fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of a simple confirmation message."),
|
|
798
867
|
}, async ({ repositoryId, pullRequestId, threadId, content, project, fullResponse }) => {
|
|
799
868
|
try {
|
|
@@ -825,10 +894,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
825
894
|
}
|
|
826
895
|
});
|
|
827
896
|
server.tool(REPO_TOOLS.create_pull_request_thread, "Creates a new comment thread on a pull request.", {
|
|
828
|
-
repositoryId: z
|
|
897
|
+
repositoryId: z
|
|
898
|
+
.string()
|
|
899
|
+
.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
900
|
pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
|
|
830
901
|
content: z.string().describe("The content of the comment to be added."),
|
|
831
|
-
project: z.string().optional().describe("Project ID or project name
|
|
902
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
832
903
|
filePath: z.string().optional().describe("The path of the file where the comment thread will be created. (optional)"),
|
|
833
904
|
status: z
|
|
834
905
|
.enum(getEnumKeys(CommentThreadStatus))
|
|
@@ -939,10 +1010,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
939
1010
|
}
|
|
940
1011
|
});
|
|
941
1012
|
server.tool(REPO_TOOLS.update_pull_request_thread, "Updates an existing comment thread on a pull request.", {
|
|
942
|
-
repositoryId: z
|
|
1013
|
+
repositoryId: z
|
|
1014
|
+
.string()
|
|
1015
|
+
.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
1016
|
pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
|
|
944
1017
|
threadId: z.number().describe("The ID of the thread to update."),
|
|
945
|
-
project: z.string().optional().describe("Project ID or project name
|
|
1018
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
946
1019
|
status: z
|
|
947
1020
|
.enum(getEnumKeys(CommentThreadStatus))
|
|
948
1021
|
.optional()
|
|
@@ -1149,5 +1222,101 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1149
1222
|
};
|
|
1150
1223
|
}
|
|
1151
1224
|
});
|
|
1225
|
+
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.", {
|
|
1226
|
+
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."),
|
|
1227
|
+
pullRequestId: z.number().describe("The ID of the pull request."),
|
|
1228
|
+
vote: z.enum(["Approved", "ApprovedWithSuggestions", "NoVote", "WaitingForAuthor", "Rejected"]).describe("The vote to cast: Approved(10), Suggestions(5), None(0), Waiting(-5), Rejected(-10)."),
|
|
1229
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
1230
|
+
}, async ({ repositoryId, pullRequestId, vote, project }) => {
|
|
1231
|
+
const connection = await connectionProvider();
|
|
1232
|
+
const gitApi = await connection.getGitApi();
|
|
1233
|
+
const userDetails = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
|
|
1234
|
+
const userId = userDetails.authenticatedUser.id;
|
|
1235
|
+
if (!userId) {
|
|
1236
|
+
throw new Error("Could not determine authenticated user ID.");
|
|
1237
|
+
}
|
|
1238
|
+
const voteMap = {
|
|
1239
|
+
Approved: 10,
|
|
1240
|
+
ApprovedWithSuggestions: 5,
|
|
1241
|
+
NoVote: 0,
|
|
1242
|
+
WaitingForAuthor: -5,
|
|
1243
|
+
Rejected: -10,
|
|
1244
|
+
};
|
|
1245
|
+
await gitApi.createPullRequestReviewer({ vote: voteMap[vote], id: userId }, repositoryId, pullRequestId, userId, project);
|
|
1246
|
+
return {
|
|
1247
|
+
content: [
|
|
1248
|
+
{
|
|
1249
|
+
type: "text",
|
|
1250
|
+
text: `Successfully cast vote '${vote}' on PR #${pullRequestId}.`,
|
|
1251
|
+
},
|
|
1252
|
+
],
|
|
1253
|
+
};
|
|
1254
|
+
});
|
|
1255
|
+
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.", {
|
|
1256
|
+
repositoryId: z.string().describe("The ID or name of the repository."),
|
|
1257
|
+
path: z.string().optional().default("/").describe("The directory path to list (e.g., '/src' or '/src/components'). Defaults to repository root."),
|
|
1258
|
+
project: z.string().optional().describe("Project ID or name. Required if repositoryId is a name rather than a GUID."),
|
|
1259
|
+
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."),
|
|
1260
|
+
versionType: z.enum(["Branch", "Commit", "Tag"]).optional().default("Branch").describe("The type of version identifier: 'Branch', 'Commit', or 'Tag'. Defaults to 'Branch'."),
|
|
1261
|
+
recursive: z.boolean().optional().default(false).describe("Whether to list items recursively. Defaults to false."),
|
|
1262
|
+
recursionDepth: z.number().optional().default(1).describe("Maximum depth for recursive listing (1-10). Only applies when recursive is true. Defaults to 1."),
|
|
1263
|
+
}, async ({ repositoryId, path, project, version, versionType, recursive, recursionDepth }) => {
|
|
1264
|
+
try {
|
|
1265
|
+
const connection = await connectionProvider();
|
|
1266
|
+
const gitApi = await connection.getGitApi();
|
|
1267
|
+
const versionDescriptor = buildVersionDescriptor(version, versionType);
|
|
1268
|
+
const clampedDepth = Math.min(Math.max(recursionDepth || 1, 1), 10);
|
|
1269
|
+
let recursionType = VersionControlRecursionType.OneLevel;
|
|
1270
|
+
if (recursive) {
|
|
1271
|
+
recursionType = VersionControlRecursionType.Full;
|
|
1272
|
+
}
|
|
1273
|
+
const items = await gitApi.getItems(repositoryId, project, path, recursionType, true, false, false, false, versionDescriptor);
|
|
1274
|
+
if (!items || items.length === 0) {
|
|
1275
|
+
return {
|
|
1276
|
+
content: [{ type: "text", text: `No items found at path: ${path}` }],
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
let filteredItems = items;
|
|
1280
|
+
if (recursive && clampedDepth < 10) {
|
|
1281
|
+
const basePath = path === "/" ? "" : path;
|
|
1282
|
+
const baseDepth = basePath.split("/").filter((p) => p).length;
|
|
1283
|
+
filteredItems = items.filter((item) => {
|
|
1284
|
+
if (!item.path)
|
|
1285
|
+
return false;
|
|
1286
|
+
const itemDepth = item.path.split("/").filter((p) => p).length;
|
|
1287
|
+
return itemDepth <= baseDepth + clampedDepth;
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
const formattedItems = filteredItems.map((item) => ({
|
|
1291
|
+
path: item.path,
|
|
1292
|
+
isFolder: item.isFolder,
|
|
1293
|
+
gitObjectType: item.gitObjectType,
|
|
1294
|
+
commitId: item.commitId,
|
|
1295
|
+
contentMetadata: item.contentMetadata
|
|
1296
|
+
? {
|
|
1297
|
+
contentType: item.contentMetadata.contentType,
|
|
1298
|
+
fileName: item.contentMetadata.fileName,
|
|
1299
|
+
}
|
|
1300
|
+
: undefined,
|
|
1301
|
+
}));
|
|
1302
|
+
const response = {
|
|
1303
|
+
count: formattedItems.length,
|
|
1304
|
+
path: path,
|
|
1305
|
+
recursive: recursive,
|
|
1306
|
+
recursionDepth: recursive ? clampedDepth : undefined,
|
|
1307
|
+
items: formattedItems,
|
|
1308
|
+
};
|
|
1309
|
+
return {
|
|
1310
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
catch (error) {
|
|
1314
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
1315
|
+
return {
|
|
1316
|
+
content: [{ type: "text", text: `Error listing directory: ${errorMessage}` }],
|
|
1317
|
+
isError: true,
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1152
1321
|
}
|
|
1153
1322
|
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) {
|