@azure-devops/mcp 2.5.0 → 2.6.0-nightly.20260419
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 +0 -12
- package/dist/auth.js +13 -0
- package/dist/index.js +23 -5
- package/dist/logger.js +0 -0
- package/dist/org-tenants.js +0 -0
- package/dist/prompts.js +0 -0
- package/dist/shared/content-safety.js +24 -0
- package/dist/shared/domains.js +7 -2
- package/dist/tools/advanced-security.js +2 -2
- package/dist/tools/core.js +5 -5
- package/dist/tools/mcp-apps.js +22 -0
- package/dist/tools/pipelines.js +33 -26
- package/dist/tools/repositories.js +536 -85
- package/dist/tools/search.js +10 -7
- package/dist/tools/test-plans.js +109 -25
- package/dist/tools/wiki.js +7 -6
- package/dist/tools/work-items.js +330 -90
- package/dist/tools/work.js +2 -2
- package/dist/tools.js +3 -1
- package/dist/useragent.js +0 -0
- package/dist/utils.js +15 -0
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
-
import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, GitPullRequestMergeStrategy, VersionControlRecursionType, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
3
|
+
import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, GitPullRequestMergeStrategy, VersionControlChangeType, VersionControlRecursionType, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
|
|
6
|
-
import { getEnumKeys } from "../utils.js";
|
|
6
|
+
import { getEnumKeys, streamToString } from "../utils.js";
|
|
7
7
|
const REPO_TOOLS = {
|
|
8
8
|
list_repos_by_project: "repo_list_repos_by_project",
|
|
9
9
|
list_pull_requests_by_repo_or_project: "repo_list_pull_requests_by_repo_or_project",
|
|
@@ -14,6 +14,7 @@ const REPO_TOOLS = {
|
|
|
14
14
|
get_repo_by_name_or_id: "repo_get_repo_by_name_or_id",
|
|
15
15
|
get_branch_by_name: "repo_get_branch_by_name",
|
|
16
16
|
get_pull_request_by_id: "repo_get_pull_request_by_id",
|
|
17
|
+
get_pull_request_changes: "repo_get_pull_request_changes",
|
|
17
18
|
create_pull_request: "repo_create_pull_request",
|
|
18
19
|
create_branch: "repo_create_branch",
|
|
19
20
|
update_pull_request: "repo_update_pull_request",
|
|
@@ -25,6 +26,7 @@ const REPO_TOOLS = {
|
|
|
25
26
|
list_pull_requests_by_commits: "repo_list_pull_requests_by_commits",
|
|
26
27
|
vote_pull_request: "repo_vote_pull_request",
|
|
27
28
|
list_directory: "repo_list_directory",
|
|
29
|
+
get_file_content: "repo_get_file_content",
|
|
28
30
|
};
|
|
29
31
|
function branchesFilterOutIrrelevantProperties(branches, top) {
|
|
30
32
|
return branches
|
|
@@ -86,6 +88,9 @@ function filterReposByName(repositories, repoNameFilter) {
|
|
|
86
88
|
return filteredByName;
|
|
87
89
|
}
|
|
88
90
|
function trimPullRequest(pr, includeDescription = false) {
|
|
91
|
+
if (!pr) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
89
94
|
return {
|
|
90
95
|
pullRequestId: pr.pullRequestId,
|
|
91
96
|
codeReviewId: pr.codeReviewId,
|
|
@@ -122,16 +127,19 @@ function buildVersionDescriptor(version, versionType) {
|
|
|
122
127
|
}
|
|
123
128
|
function configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
124
129
|
server.tool(REPO_TOOLS.create_pull_request, "Create a new pull request.", {
|
|
125
|
-
repositoryId: z
|
|
130
|
+
repositoryId: z
|
|
131
|
+
.string()
|
|
132
|
+
.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
133
|
sourceRefName: z.string().describe("The source branch name for the pull request, e.g., 'refs/heads/feature-branch'."),
|
|
127
134
|
targetRefName: z.string().describe("The target branch name for the pull request, e.g., 'refs/heads/main'."),
|
|
128
135
|
title: z.string().describe("The title of the pull request."),
|
|
129
136
|
description: z.string().max(4000).optional().describe("The description of the pull request. Must not be longer than 4000 characters. Optional."),
|
|
130
137
|
isDraft: z.boolean().optional().default(false).describe("Indicates whether the pull request is a draft. Defaults to false."),
|
|
138
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
131
139
|
workItems: z.string().optional().describe("Work item IDs to associate with the pull request, space-separated."),
|
|
132
140
|
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
141
|
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 }) => {
|
|
142
|
+
}, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, project, workItems, forkSourceRepositoryId, labels }) => {
|
|
135
143
|
try {
|
|
136
144
|
const connection = await connectionProvider();
|
|
137
145
|
const gitApi = await connection.getGitApi();
|
|
@@ -154,9 +162,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
154
162
|
forkSource,
|
|
155
163
|
labels: labelDefinitions,
|
|
156
164
|
supportsIterations: true,
|
|
157
|
-
}, repositoryId);
|
|
165
|
+
}, repositoryId, project);
|
|
158
166
|
if (!pullRequest) {
|
|
159
|
-
const prs = await gitApi.getPullRequests(repositoryId, { sourceRefName, targetRefName, status: PullRequestStatus.Active },
|
|
167
|
+
const prs = await gitApi.getPullRequests(repositoryId, { sourceRefName, targetRefName, status: PullRequestStatus.Active }, project, undefined, 0, 1);
|
|
160
168
|
if (prs && prs.length > 0) {
|
|
161
169
|
pullRequest = prs[0];
|
|
162
170
|
}
|
|
@@ -167,6 +175,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
167
175
|
}
|
|
168
176
|
}
|
|
169
177
|
const trimmedPullRequest = trimPullRequest(pullRequest, true);
|
|
178
|
+
if (!trimmedPullRequest) {
|
|
179
|
+
return {
|
|
180
|
+
content: [{ type: "text", text: "Pull request created but API returned no data." }],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
170
183
|
return {
|
|
171
184
|
content: [{ type: "text", text: JSON.stringify(trimmedPullRequest, null, 2) }],
|
|
172
185
|
};
|
|
@@ -180,11 +193,14 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
180
193
|
}
|
|
181
194
|
});
|
|
182
195
|
server.tool(REPO_TOOLS.create_branch, "Create a new branch in the repository.", {
|
|
183
|
-
repositoryId: z
|
|
196
|
+
repositoryId: z
|
|
197
|
+
.string()
|
|
198
|
+
.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
199
|
branchName: z.string().describe("The name of the new branch to create, e.g., 'feature-branch'."),
|
|
185
200
|
sourceBranchName: z.string().optional().default("main").describe("The name of the source branch to create the new branch from. Defaults to 'main'."),
|
|
186
201
|
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
|
-
|
|
202
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
203
|
+
}, async ({ repositoryId, branchName, sourceBranchName, sourceCommitId, project }) => {
|
|
188
204
|
try {
|
|
189
205
|
const connection = await connectionProvider();
|
|
190
206
|
const gitApi = await connection.getGitApi();
|
|
@@ -193,7 +209,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
193
209
|
if (!commitId) {
|
|
194
210
|
const sourceRefName = `refs/heads/${sourceBranchName}`;
|
|
195
211
|
try {
|
|
196
|
-
const sourceBranch = await gitApi.getRefs(repositoryId,
|
|
212
|
+
const sourceBranch = await gitApi.getRefs(repositoryId, project, "heads/", false, false, undefined, false, undefined, sourceBranchName);
|
|
197
213
|
const branch = sourceBranch.find((b) => b.name === sourceRefName);
|
|
198
214
|
if (!branch || !branch.objectId) {
|
|
199
215
|
return {
|
|
@@ -228,7 +244,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
228
244
|
oldObjectId: "0000000000000000000000000000000000000000", // All zeros indicates creating a new ref
|
|
229
245
|
};
|
|
230
246
|
try {
|
|
231
|
-
const result = await gitApi.updateRefs([refUpdate], repositoryId);
|
|
247
|
+
const result = await gitApi.updateRefs([refUpdate], repositoryId, project);
|
|
232
248
|
// Check if the branch creation was successful
|
|
233
249
|
if (result && result.length > 0 && result[0].success) {
|
|
234
250
|
return {
|
|
@@ -274,8 +290,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
274
290
|
}
|
|
275
291
|
});
|
|
276
292
|
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."),
|
|
278
|
-
pullRequestId: z.number().describe("The ID of the pull request to update."),
|
|
293
|
+
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."),
|
|
294
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request to update."),
|
|
295
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
279
296
|
title: z.string().optional().describe("The new title for the pull request."),
|
|
280
297
|
description: z.string().max(4000).optional().describe("The new description for the pull request. Must not be longer than 4000 characters."),
|
|
281
298
|
isDraft: z.boolean().optional().describe("Whether the pull request should be a draft."),
|
|
@@ -290,7 +307,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
290
307
|
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
308
|
bypassReason: z.string().optional().describe("Reason for bypassing branch policies. When provided, branch policies will be automatically bypassed during autocompletion."),
|
|
292
309
|
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 }) => {
|
|
310
|
+
}, async ({ repositoryId, pullRequestId, project, title, description, isDraft, targetRefName, status, autoComplete, mergeStrategy, deleteSourceBranch, transitionWorkItems, bypassReason, labels, }) => {
|
|
294
311
|
try {
|
|
295
312
|
const connection = await connectionProvider();
|
|
296
313
|
const gitApi = await connection.getGitApi();
|
|
@@ -339,25 +356,30 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
339
356
|
}
|
|
340
357
|
// Update labels if provided
|
|
341
358
|
if (labels) {
|
|
342
|
-
const currentLabels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId);
|
|
359
|
+
const currentLabels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId, project);
|
|
343
360
|
for (const currentLabel of currentLabels) {
|
|
344
361
|
if (currentLabel.id) {
|
|
345
|
-
await gitApi.deletePullRequestLabels(repositoryId, pullRequestId, currentLabel.id);
|
|
362
|
+
await gitApi.deletePullRequestLabels(repositoryId, pullRequestId, currentLabel.id, project);
|
|
346
363
|
}
|
|
347
364
|
}
|
|
348
365
|
for (const label of labels) {
|
|
349
|
-
await gitApi.createPullRequestLabel({ name: label }, repositoryId, pullRequestId);
|
|
366
|
+
await gitApi.createPullRequestLabel({ name: label }, repositoryId, pullRequestId, project);
|
|
350
367
|
}
|
|
351
368
|
}
|
|
352
369
|
let updatedPullRequest;
|
|
353
370
|
if (Object.keys(updateRequest).length > 0) {
|
|
354
|
-
updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId);
|
|
371
|
+
updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId, project);
|
|
355
372
|
}
|
|
356
373
|
else {
|
|
357
374
|
// If only labels were updated, get the current pull request
|
|
358
|
-
updatedPullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId);
|
|
375
|
+
updatedPullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, project);
|
|
359
376
|
}
|
|
360
377
|
const trimmedUpdatedPullRequest = trimPullRequest(updatedPullRequest, true);
|
|
378
|
+
if (!trimmedUpdatedPullRequest) {
|
|
379
|
+
return {
|
|
380
|
+
content: [{ type: "text", text: "Pull request updated but API returned no data." }],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
361
383
|
return {
|
|
362
384
|
content: [{ type: "text", text: JSON.stringify(trimmedUpdatedPullRequest, null, 2) }],
|
|
363
385
|
};
|
|
@@ -371,17 +393,18 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
371
393
|
}
|
|
372
394
|
});
|
|
373
395
|
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."),
|
|
375
|
-
pullRequestId: z.number().describe("The ID of the pull request to update."),
|
|
396
|
+
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."),
|
|
397
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request to update."),
|
|
376
398
|
reviewerIds: z.array(z.string()).describe("List of reviewer ids to add or remove from the pull request."),
|
|
377
399
|
action: z.enum(["add", "remove"]).describe("Action to perform on the reviewers. Can be 'add' or 'remove'."),
|
|
378
|
-
|
|
400
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
401
|
+
}, async ({ repositoryId, pullRequestId, reviewerIds, action, project }) => {
|
|
379
402
|
try {
|
|
380
403
|
const connection = await connectionProvider();
|
|
381
404
|
const gitApi = await connection.getGitApi();
|
|
382
405
|
let updatedPullRequest;
|
|
383
406
|
if (action === "add") {
|
|
384
|
-
updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId);
|
|
407
|
+
updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId, project);
|
|
385
408
|
const trimmedResponse = updatedPullRequest.map((item) => ({
|
|
386
409
|
displayName: item.displayName,
|
|
387
410
|
id: item.id,
|
|
@@ -396,7 +419,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
396
419
|
}
|
|
397
420
|
else {
|
|
398
421
|
for (const reviewerId of reviewerIds) {
|
|
399
|
-
await gitApi.deletePullRequestReviewer(repositoryId, pullRequestId, reviewerId);
|
|
422
|
+
await gitApi.deletePullRequestReviewer(repositoryId, pullRequestId, reviewerId, project);
|
|
400
423
|
}
|
|
401
424
|
return {
|
|
402
425
|
content: [{ type: "text", text: `Reviewers with IDs ${reviewerIds.join(", ")} removed from pull request ${pullRequestId}.` }],
|
|
@@ -413,8 +436,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
413
436
|
});
|
|
414
437
|
server.tool(REPO_TOOLS.list_repos_by_project, "Retrieve a list of repositories for a given project", {
|
|
415
438
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
416
|
-
top: z.number().default(100).describe("The maximum number of repositories to return."),
|
|
417
|
-
skip: z.number().default(0).describe("The number of repositories to skip. Defaults to 0."),
|
|
439
|
+
top: z.coerce.number().default(100).describe("The maximum number of repositories to return."),
|
|
440
|
+
skip: z.coerce.number().default(0).describe("The number of repositories to skip. Defaults to 0."),
|
|
418
441
|
repoNameFilter: z.string().optional().describe("Optional filter to search for repositories by name. If provided, only repositories with names containing this string will be returned."),
|
|
419
442
|
}, async ({ project, top, skip, repoNameFilter }) => {
|
|
420
443
|
try {
|
|
@@ -446,10 +469,13 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
446
469
|
}
|
|
447
470
|
});
|
|
448
471
|
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
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
472
|
+
repositoryId: z
|
|
473
|
+
.string()
|
|
474
|
+
.optional()
|
|
475
|
+
.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."),
|
|
476
|
+
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."),
|
|
477
|
+
top: z.coerce.number().default(100).describe("The maximum number of pull requests to return."),
|
|
478
|
+
skip: z.coerce.number().default(0).describe("The number of pull requests to skip."),
|
|
453
479
|
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
|
|
454
480
|
created_by_user: z.string().optional().describe("Filter pull requests created by a specific user (provide email or unique name). Takes precedence over created_by_me if both are provided."),
|
|
455
481
|
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
|
|
@@ -572,13 +598,15 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
572
598
|
}
|
|
573
599
|
});
|
|
574
600
|
server.tool(REPO_TOOLS.list_pull_request_threads, "Retrieve a list of comment threads for a pull request.", {
|
|
575
|
-
repositoryId: z
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
601
|
+
repositoryId: z
|
|
602
|
+
.string()
|
|
603
|
+
.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."),
|
|
604
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request for which to retrieve threads."),
|
|
605
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
606
|
+
iteration: z.coerce.number().min(1).optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
|
|
607
|
+
baseIteration: z.coerce.number().min(1).optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
|
|
608
|
+
top: z.coerce.number().default(100).describe("The maximum number of threads to return after filtering."),
|
|
609
|
+
skip: z.coerce.number().default(0).describe("The number of threads to skip after filtering."),
|
|
582
610
|
fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of trimmed data."),
|
|
583
611
|
status: z
|
|
584
612
|
.enum(getEnumKeys(CommentThreadStatus))
|
|
@@ -630,12 +658,14 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
630
658
|
}
|
|
631
659
|
});
|
|
632
660
|
server.tool(REPO_TOOLS.list_pull_request_thread_comments, "Retrieve a list of comments in a pull request thread.", {
|
|
633
|
-
repositoryId: z
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
661
|
+
repositoryId: z
|
|
662
|
+
.string()
|
|
663
|
+
.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."),
|
|
664
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request for which to retrieve thread comments."),
|
|
665
|
+
threadId: z.coerce.number().min(1).describe("The ID of the thread for which to retrieve comments."),
|
|
666
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
667
|
+
top: z.coerce.number().default(100).describe("The maximum number of comments to return."),
|
|
668
|
+
skip: z.coerce.number().default(0).describe("The number of comments to skip."),
|
|
639
669
|
fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of trimmed data."),
|
|
640
670
|
}, async ({ repositoryId, pullRequestId, threadId, project, top, skip, fullResponse }) => {
|
|
641
671
|
try {
|
|
@@ -664,14 +694,17 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
664
694
|
}
|
|
665
695
|
});
|
|
666
696
|
server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
|
|
667
|
-
repositoryId: z
|
|
668
|
-
|
|
697
|
+
repositoryId: z
|
|
698
|
+
.string()
|
|
699
|
+
.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."),
|
|
700
|
+
top: z.coerce.number().default(100).describe("The maximum number of branches to return. Defaults to 100."),
|
|
669
701
|
filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
|
|
670
|
-
|
|
702
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
703
|
+
}, async ({ repositoryId, top, filterContains, project }) => {
|
|
671
704
|
try {
|
|
672
705
|
const connection = await connectionProvider();
|
|
673
706
|
const gitApi = await connection.getGitApi();
|
|
674
|
-
const branches = await gitApi.getRefs(repositoryId,
|
|
707
|
+
const branches = await gitApi.getRefs(repositoryId, project, "heads/", undefined, undefined, undefined, undefined, undefined, filterContains);
|
|
675
708
|
const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
|
|
676
709
|
return {
|
|
677
710
|
content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
|
|
@@ -686,14 +719,17 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
686
719
|
}
|
|
687
720
|
});
|
|
688
721
|
server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branches for a given repository Id.", {
|
|
689
|
-
repositoryId: z
|
|
690
|
-
|
|
722
|
+
repositoryId: z
|
|
723
|
+
.string()
|
|
724
|
+
.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."),
|
|
725
|
+
top: z.coerce.number().default(100).describe("The maximum number of branches to return."),
|
|
691
726
|
filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
|
|
692
|
-
|
|
727
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
728
|
+
}, async ({ repositoryId, top, filterContains, project }) => {
|
|
693
729
|
try {
|
|
694
730
|
const connection = await connectionProvider();
|
|
695
731
|
const gitApi = await connection.getGitApi();
|
|
696
|
-
const branches = await gitApi.getRefs(repositoryId,
|
|
732
|
+
const branches = await gitApi.getRefs(repositoryId, project, "heads/", undefined, undefined, true, undefined, undefined, filterContains);
|
|
697
733
|
const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
|
|
698
734
|
return {
|
|
699
735
|
content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
|
|
@@ -735,13 +771,14 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
735
771
|
}
|
|
736
772
|
});
|
|
737
773
|
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."),
|
|
774
|
+
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
775
|
branchName: z.string().describe("The name of the branch to retrieve, e.g., 'main' or 'feature-branch'."),
|
|
740
|
-
|
|
776
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
777
|
+
}, async ({ repositoryId, branchName, project }) => {
|
|
741
778
|
try {
|
|
742
779
|
const connection = await connectionProvider();
|
|
743
780
|
const gitApi = await connection.getGitApi();
|
|
744
|
-
const branches = await gitApi.getRefs(repositoryId,
|
|
781
|
+
const branches = await gitApi.getRefs(repositoryId, project, "heads/", false, false, undefined, false, undefined, branchName);
|
|
745
782
|
const branch = branches.find((branch) => branch.name === `refs/heads/${branchName}` || branch.name === branchName);
|
|
746
783
|
if (!branch) {
|
|
747
784
|
return {
|
|
@@ -770,46 +807,80 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
770
807
|
repositoryId: z
|
|
771
808
|
.string()
|
|
772
809
|
.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."),
|
|
773
|
-
pullRequestId: z.number().describe("The ID of the pull request to retrieve."),
|
|
810
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request to retrieve."),
|
|
774
811
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
775
812
|
includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to reference work items associated with the pull request."),
|
|
776
813
|
includeLabels: z.boolean().optional().default(false).describe("Whether to include a summary of labels in the response."),
|
|
777
|
-
|
|
814
|
+
includeChangedFiles: z.boolean().optional().default(false).describe("Whether to include the list of files changed in the pull request."),
|
|
815
|
+
}, async ({ repositoryId, pullRequestId, project, includeWorkItemRefs, includeLabels, includeChangedFiles }) => {
|
|
778
816
|
try {
|
|
779
817
|
const connection = await connectionProvider();
|
|
780
818
|
const gitApi = await connection.getGitApi();
|
|
781
819
|
const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, project, undefined, undefined, undefined, undefined, includeWorkItemRefs);
|
|
820
|
+
let enhancedResponse = { ...pullRequest };
|
|
782
821
|
if (includeLabels) {
|
|
783
822
|
try {
|
|
784
823
|
const projectId = pullRequest.repository?.project?.id;
|
|
785
824
|
const projectName = pullRequest.repository?.project?.name;
|
|
786
825
|
const labels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId, projectName, projectId);
|
|
787
826
|
const labelNames = labels.map((label) => label.name).filter((name) => name !== undefined);
|
|
788
|
-
|
|
789
|
-
...
|
|
827
|
+
enhancedResponse = {
|
|
828
|
+
...enhancedResponse,
|
|
790
829
|
labelSummary: {
|
|
791
830
|
labels: labelNames,
|
|
792
831
|
labelCount: labelNames.length,
|
|
793
832
|
},
|
|
794
833
|
};
|
|
795
|
-
return {
|
|
796
|
-
content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
|
|
797
|
-
};
|
|
798
834
|
}
|
|
799
835
|
catch (error) {
|
|
800
836
|
console.warn(`Error fetching PR labels: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
...pullRequest,
|
|
837
|
+
enhancedResponse = {
|
|
838
|
+
...enhancedResponse,
|
|
804
839
|
labelSummary: {},
|
|
805
840
|
};
|
|
806
|
-
|
|
807
|
-
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (includeChangedFiles) {
|
|
844
|
+
try {
|
|
845
|
+
const iterations = await gitApi.getPullRequestIterations(repositoryId, pullRequestId, project);
|
|
846
|
+
if (iterations?.length) {
|
|
847
|
+
const latestIteration = iterations[iterations.length - 1];
|
|
848
|
+
if (latestIteration.id != null) {
|
|
849
|
+
const changes = await gitApi.getPullRequestIterationChanges(repositoryId, pullRequestId, latestIteration.id, project);
|
|
850
|
+
enhancedResponse = {
|
|
851
|
+
...enhancedResponse,
|
|
852
|
+
changedFilesSummary: {
|
|
853
|
+
changeEntries: changes?.changeEntries ?? [],
|
|
854
|
+
fileCount: changes?.changeEntries?.length ?? 0,
|
|
855
|
+
nextSkip: changes?.nextSkip,
|
|
856
|
+
nextTop: changes?.nextTop,
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
enhancedResponse = {
|
|
862
|
+
...enhancedResponse,
|
|
863
|
+
changedFilesSummary: { changeEntries: [], fileCount: 0 },
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
enhancedResponse = {
|
|
869
|
+
...enhancedResponse,
|
|
870
|
+
changedFilesSummary: { changeEntries: [], fileCount: 0 },
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
console.warn(`Error fetching PR changed files: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
876
|
+
enhancedResponse = {
|
|
877
|
+
...enhancedResponse,
|
|
878
|
+
changedFilesSummary: {},
|
|
808
879
|
};
|
|
809
880
|
}
|
|
810
881
|
}
|
|
811
882
|
return {
|
|
812
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
883
|
+
content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
|
|
813
884
|
};
|
|
814
885
|
}
|
|
815
886
|
catch (error) {
|
|
@@ -820,12 +891,330 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
820
891
|
};
|
|
821
892
|
}
|
|
822
893
|
});
|
|
823
|
-
server.tool(REPO_TOOLS.
|
|
894
|
+
server.tool(REPO_TOOLS.get_pull_request_changes, "Get the file changes (diff) for a pull request iteration with actual code diff content. Returns the code changes including line-by-line diffs made in the pull request.", {
|
|
824
895
|
repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
|
|
825
|
-
pullRequestId: z.number().describe("The ID of the pull request
|
|
826
|
-
|
|
827
|
-
content: z.string().describe("The content of the comment to be added."),
|
|
896
|
+
pullRequestId: z.number().describe("The ID of the pull request to retrieve changes for."),
|
|
897
|
+
iterationId: z.number().optional().describe("The iteration ID to get changes for. If not specified, gets changes for the latest iteration."),
|
|
828
898
|
project: z.string().optional().describe("Project ID or project name (optional)"),
|
|
899
|
+
top: z.number().optional().describe("Maximum number of files to include diffs for. Default is 100."),
|
|
900
|
+
skip: z.number().optional().describe("Number of changes to skip for pagination."),
|
|
901
|
+
compareTo: z.number().optional().describe("Iteration ID to compare against. If specified, returns changes between two iterations."),
|
|
902
|
+
includeDiffs: z.boolean().optional().describe("Whether to include actual line-by-line diff content. Default is true. Set to false to get only file metadata."),
|
|
903
|
+
includeLineContent: z
|
|
904
|
+
.boolean()
|
|
905
|
+
.optional()
|
|
906
|
+
.describe("Whether to include the actual line content from the changed files. Default is true. When true, fetches file content and includes the actual code lines that were added/removed/modified."),
|
|
907
|
+
}, async ({ repositoryId, pullRequestId, iterationId, project, top, skip, compareTo, includeDiffs = true, includeLineContent = true }) => {
|
|
908
|
+
try {
|
|
909
|
+
const connection = await connectionProvider();
|
|
910
|
+
const gitApi = await connection.getGitApi();
|
|
911
|
+
// If repositoryId is a name (not a GUID), we need a project to resolve it.
|
|
912
|
+
// GUID pattern: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
913
|
+
const isGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(repositoryId);
|
|
914
|
+
if (!isGuid && !project) {
|
|
915
|
+
return {
|
|
916
|
+
content: [
|
|
917
|
+
{
|
|
918
|
+
type: "text",
|
|
919
|
+
text: "Error: When using a repository name instead of a GUID for repositoryId, the 'project' parameter is required. Please either provide the project name/ID, or use repo_get_repo_by_name_or_id to resolve the repository GUID first.",
|
|
920
|
+
},
|
|
921
|
+
],
|
|
922
|
+
isError: true,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
// If no iteration ID provided, get the latest iteration
|
|
926
|
+
let targetIterationId = iterationId;
|
|
927
|
+
let targetIteration;
|
|
928
|
+
if (targetIterationId == null) {
|
|
929
|
+
const iterations = await gitApi.getPullRequestIterations(repositoryId, pullRequestId, project);
|
|
930
|
+
if (!iterations || iterations.length === 0) {
|
|
931
|
+
return {
|
|
932
|
+
content: [{ type: "text", text: "No iterations found for this pull request." }],
|
|
933
|
+
isError: true,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
// Get the latest iteration
|
|
937
|
+
targetIteration = iterations[iterations.length - 1];
|
|
938
|
+
targetIterationId = targetIteration.id;
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
// Get the specific iteration
|
|
942
|
+
targetIteration = await gitApi.getPullRequestIteration(repositoryId, pullRequestId, targetIterationId, project);
|
|
943
|
+
}
|
|
944
|
+
// Get the file change metadata
|
|
945
|
+
const changes = await gitApi.getPullRequestIterationChanges(repositoryId, pullRequestId, targetIterationId ?? 1, project, top, skip, compareTo);
|
|
946
|
+
// If includeDiffs is false, just return the metadata
|
|
947
|
+
if (!includeDiffs) {
|
|
948
|
+
return {
|
|
949
|
+
content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
// Get actual diff content using getFileDiffs
|
|
953
|
+
if (changes.changeEntries && changes.changeEntries.length > 0 && targetIteration) {
|
|
954
|
+
// Determine base and target commits
|
|
955
|
+
const baseCommitId = compareTo
|
|
956
|
+
? (await gitApi.getPullRequestIteration(repositoryId, pullRequestId, compareTo, project)).sourceRefCommit?.commitId
|
|
957
|
+
: targetIteration.commonRefCommit?.commitId;
|
|
958
|
+
const targetCommitId = targetIteration.sourceRefCommit?.commitId;
|
|
959
|
+
if (baseCommitId && targetCommitId) {
|
|
960
|
+
// Build FileDiffsCriteria with paths from changeEntries
|
|
961
|
+
// Exclude added and deleted files as they don't have both versions to diff
|
|
962
|
+
// changeType is a flags enum so use bitwise AND to check
|
|
963
|
+
const fileDiffParams = changes.changeEntries
|
|
964
|
+
.filter((entry) => {
|
|
965
|
+
const ct = entry.changeType ?? 0;
|
|
966
|
+
return entry.item?.path && !(ct & VersionControlChangeType.Add) && !(ct & VersionControlChangeType.Delete);
|
|
967
|
+
})
|
|
968
|
+
.map((entry) => {
|
|
969
|
+
// Remove leading slash if present - Azure DevOps API expects relative paths
|
|
970
|
+
const itemPath = entry.item?.path ?? "";
|
|
971
|
+
const path = itemPath.startsWith("/") ? itemPath.substring(1) : itemPath;
|
|
972
|
+
// For renamed/moved files, use the original path from the change entry
|
|
973
|
+
const origPath = entry.originalPath ? (entry.originalPath.startsWith("/") ? entry.originalPath.substring(1) : entry.originalPath) : path;
|
|
974
|
+
return {
|
|
975
|
+
path: path,
|
|
976
|
+
originalPath: origPath,
|
|
977
|
+
};
|
|
978
|
+
});
|
|
979
|
+
if (fileDiffParams.length > 0) {
|
|
980
|
+
try {
|
|
981
|
+
// Azure DevOps getFileDiffs API accepts max 10 files per request
|
|
982
|
+
const FILE_DIFF_BATCH_SIZE = 10;
|
|
983
|
+
let fileDiffs = [];
|
|
984
|
+
for (let i = 0; i < fileDiffParams.length; i += FILE_DIFF_BATCH_SIZE) {
|
|
985
|
+
const batch = fileDiffParams.slice(i, i + FILE_DIFF_BATCH_SIZE);
|
|
986
|
+
const batchDiffs = await gitApi.getFileDiffs({
|
|
987
|
+
baseVersionCommit: baseCommitId,
|
|
988
|
+
targetVersionCommit: targetCommitId,
|
|
989
|
+
fileDiffParams: batch,
|
|
990
|
+
}, project || "", repositoryId);
|
|
991
|
+
fileDiffs = fileDiffs.concat(batchDiffs);
|
|
992
|
+
}
|
|
993
|
+
// Merge diff content with change metadata
|
|
994
|
+
const enrichedChanges = {
|
|
995
|
+
...changes,
|
|
996
|
+
changeEntries: changes.changeEntries.map((entry) => {
|
|
997
|
+
// Normalize path for comparison (remove leading slash)
|
|
998
|
+
const entryPath = entry.item?.path?.startsWith("/") ? entry.item.path.substring(1) : entry.item?.path;
|
|
999
|
+
const matchingDiff = fileDiffs.find((diff) => diff.path === entryPath);
|
|
1000
|
+
return {
|
|
1001
|
+
...entry,
|
|
1002
|
+
diff: matchingDiff || null,
|
|
1003
|
+
};
|
|
1004
|
+
}),
|
|
1005
|
+
};
|
|
1006
|
+
// If includeLineContent is true, fetch actual file content with concurrency limit
|
|
1007
|
+
if (includeLineContent && enrichedChanges.changeEntries) {
|
|
1008
|
+
const CONCURRENCY_LIMIT = 10;
|
|
1009
|
+
const entriesWithContent = [...enrichedChanges.changeEntries];
|
|
1010
|
+
for (let i = 0; i < entriesWithContent.length; i += CONCURRENCY_LIMIT) {
|
|
1011
|
+
const batch = entriesWithContent.slice(i, i + CONCURRENCY_LIMIT);
|
|
1012
|
+
const batchResults = await Promise.all(batch.map(async (entry) => {
|
|
1013
|
+
const ct = entry.changeType ?? 0;
|
|
1014
|
+
const isAdd = !!(ct & VersionControlChangeType.Add);
|
|
1015
|
+
const isDelete = !!(ct & VersionControlChangeType.Delete);
|
|
1016
|
+
const entryPath = entry.item?.path?.startsWith("/") ? entry.item.path.substring(1) : entry.item?.path;
|
|
1017
|
+
if (!entryPath) {
|
|
1018
|
+
return entry;
|
|
1019
|
+
}
|
|
1020
|
+
// Handle added files: fetch full content at target commit and create synthetic diff
|
|
1021
|
+
if (isAdd && !entry.diff) {
|
|
1022
|
+
try {
|
|
1023
|
+
const targetStream = await gitApi
|
|
1024
|
+
.getItemText(repositoryId, entryPath, project, undefined, undefined, undefined, undefined, undefined, { version: targetCommitId, versionType: GitVersionType.Commit })
|
|
1025
|
+
.catch(() => null);
|
|
1026
|
+
if (targetStream) {
|
|
1027
|
+
const targetText = await streamToString(targetStream);
|
|
1028
|
+
const targetLines = targetText.split(/\r?\n/);
|
|
1029
|
+
return {
|
|
1030
|
+
...entry,
|
|
1031
|
+
diff: {
|
|
1032
|
+
path: entryPath,
|
|
1033
|
+
originalPath: entryPath,
|
|
1034
|
+
lineDiffBlocks: [
|
|
1035
|
+
{
|
|
1036
|
+
changeType: 1, // Add
|
|
1037
|
+
originalLineNumberStart: 0,
|
|
1038
|
+
originalLinesCount: 0,
|
|
1039
|
+
modifiedLineNumberStart: 1,
|
|
1040
|
+
modifiedLinesCount: targetLines.length,
|
|
1041
|
+
modifiedLines: targetLines,
|
|
1042
|
+
},
|
|
1043
|
+
],
|
|
1044
|
+
},
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
catch (addError) {
|
|
1049
|
+
return {
|
|
1050
|
+
...entry,
|
|
1051
|
+
_contentFetchError: `Failed to fetch added file content: ${addError instanceof Error ? addError.message : "Unknown error"}`,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
return entry;
|
|
1055
|
+
}
|
|
1056
|
+
// Handle deleted files: fetch full content at base commit and create synthetic diff
|
|
1057
|
+
if (isDelete && !entry.diff) {
|
|
1058
|
+
try {
|
|
1059
|
+
const basePath = entry.originalPath ? (entry.originalPath.startsWith("/") ? entry.originalPath.substring(1) : entry.originalPath) : entryPath;
|
|
1060
|
+
const baseStream = await gitApi
|
|
1061
|
+
.getItemText(repositoryId, basePath, project, undefined, undefined, undefined, undefined, undefined, { version: baseCommitId, versionType: GitVersionType.Commit })
|
|
1062
|
+
.catch(() => null);
|
|
1063
|
+
if (baseStream) {
|
|
1064
|
+
const baseText = await streamToString(baseStream);
|
|
1065
|
+
const baseLines = baseText.split(/\r?\n/);
|
|
1066
|
+
return {
|
|
1067
|
+
...entry,
|
|
1068
|
+
diff: {
|
|
1069
|
+
path: entryPath,
|
|
1070
|
+
originalPath: basePath,
|
|
1071
|
+
lineDiffBlocks: [
|
|
1072
|
+
{
|
|
1073
|
+
changeType: 2, // Delete
|
|
1074
|
+
originalLineNumberStart: 1,
|
|
1075
|
+
originalLinesCount: baseLines.length,
|
|
1076
|
+
modifiedLineNumberStart: 0,
|
|
1077
|
+
modifiedLinesCount: 0,
|
|
1078
|
+
originalLines: baseLines,
|
|
1079
|
+
},
|
|
1080
|
+
],
|
|
1081
|
+
},
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
catch (delError) {
|
|
1086
|
+
return {
|
|
1087
|
+
...entry,
|
|
1088
|
+
_contentFetchError: `Failed to fetch deleted file content: ${delError instanceof Error ? delError.message : "Unknown error"}`,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
return entry;
|
|
1092
|
+
}
|
|
1093
|
+
// For modified/renamed files, skip if no diff blocks
|
|
1094
|
+
if (!entry.diff?.lineDiffBlocks || entry.diff.lineDiffBlocks.length === 0) {
|
|
1095
|
+
return entry;
|
|
1096
|
+
}
|
|
1097
|
+
// For renamed/moved files, the base version is at the original path
|
|
1098
|
+
const basePath = entry.originalPath ? (entry.originalPath.startsWith("/") ? entry.originalPath.substring(1) : entry.originalPath) : entryPath;
|
|
1099
|
+
try {
|
|
1100
|
+
// Fetch file content at both commits
|
|
1101
|
+
const [baseContent, targetContent] = await Promise.all([
|
|
1102
|
+
// Base version (original) - use basePath for renamed files
|
|
1103
|
+
gitApi
|
|
1104
|
+
.getItemText(repositoryId, basePath, project, undefined, undefined, undefined, undefined, undefined, { version: baseCommitId, versionType: GitVersionType.Commit })
|
|
1105
|
+
.catch(() => null),
|
|
1106
|
+
// Target version (modified)
|
|
1107
|
+
gitApi
|
|
1108
|
+
.getItemText(repositoryId, entryPath, project, undefined, undefined, undefined, undefined, undefined, { version: targetCommitId, versionType: GitVersionType.Commit })
|
|
1109
|
+
.catch(() => null),
|
|
1110
|
+
]);
|
|
1111
|
+
// Convert streams to text
|
|
1112
|
+
const baseText = baseContent ? await streamToString(baseContent) : "";
|
|
1113
|
+
const targetText = targetContent ? await streamToString(targetContent) : "";
|
|
1114
|
+
// Check if response is an Azure DevOps error (returned as JSON in the stream)
|
|
1115
|
+
const checkForApiError = (text, label) => {
|
|
1116
|
+
if (text.startsWith("{")) {
|
|
1117
|
+
try {
|
|
1118
|
+
const parsed = JSON.parse(text);
|
|
1119
|
+
if (parsed.$id && parsed.innerException !== undefined) {
|
|
1120
|
+
throw new Error(`Failed to fetch ${label} file content: ${parsed.message || text}`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
catch (e) {
|
|
1124
|
+
if (e instanceof Error && e.message.startsWith("Failed to fetch"))
|
|
1125
|
+
throw e;
|
|
1126
|
+
// Not valid JSON or not an error response — treat as legitimate content
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
checkForApiError(baseText, "base");
|
|
1131
|
+
checkForApiError(targetText, "target");
|
|
1132
|
+
// Split into lines
|
|
1133
|
+
const baseLines = baseText.split(/\r?\n/);
|
|
1134
|
+
const targetLines = targetText.split(/\r?\n/);
|
|
1135
|
+
// Enrich each lineDiffBlock with actual line content
|
|
1136
|
+
const enrichedDiff = {
|
|
1137
|
+
...entry.diff,
|
|
1138
|
+
lineDiffBlocks: entry.diff.lineDiffBlocks?.map((block) => {
|
|
1139
|
+
const enrichedBlock = { ...block };
|
|
1140
|
+
// Add original (base) lines if they exist
|
|
1141
|
+
if (block.originalLineNumberStart && block.originalLinesCount) {
|
|
1142
|
+
const startIdx = block.originalLineNumberStart - 1;
|
|
1143
|
+
const endIdx = startIdx + block.originalLinesCount;
|
|
1144
|
+
enrichedBlock.originalLines = baseLines.slice(startIdx, endIdx);
|
|
1145
|
+
}
|
|
1146
|
+
// Add modified (target) lines if they exist
|
|
1147
|
+
if (block.modifiedLineNumberStart && block.modifiedLinesCount) {
|
|
1148
|
+
const startIdx = block.modifiedLineNumberStart - 1;
|
|
1149
|
+
const endIdx = startIdx + block.modifiedLinesCount;
|
|
1150
|
+
enrichedBlock.modifiedLines = targetLines.slice(startIdx, endIdx);
|
|
1151
|
+
}
|
|
1152
|
+
return enrichedBlock;
|
|
1153
|
+
}),
|
|
1154
|
+
};
|
|
1155
|
+
return {
|
|
1156
|
+
...entry,
|
|
1157
|
+
diff: enrichedDiff,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
catch (contentError) {
|
|
1161
|
+
// If content fetch fails, return entry with error
|
|
1162
|
+
return {
|
|
1163
|
+
...entry,
|
|
1164
|
+
_contentFetchError: `Failed to fetch line content: ${contentError instanceof Error ? contentError.message : "Unknown error"}`,
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
}));
|
|
1168
|
+
// Write batch results back into the array
|
|
1169
|
+
for (let j = 0; j < batchResults.length; j++) {
|
|
1170
|
+
entriesWithContent[i + j] = batchResults[j];
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
enrichedChanges.changeEntries = entriesWithContent;
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
content: [{ type: "text", text: JSON.stringify(enrichedChanges, null, 2) }],
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
catch (diffError) {
|
|
1180
|
+
// If diff fetching fails, return metadata with error info
|
|
1181
|
+
return {
|
|
1182
|
+
content: [
|
|
1183
|
+
{
|
|
1184
|
+
type: "text",
|
|
1185
|
+
text: JSON.stringify({
|
|
1186
|
+
...changes,
|
|
1187
|
+
_diffError: `Failed to fetch diff content: ${diffError instanceof Error ? diffError.message : "Unknown error"}`,
|
|
1188
|
+
_note: "Returned metadata only",
|
|
1189
|
+
}, null, 2),
|
|
1190
|
+
},
|
|
1191
|
+
],
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
// Fallback: return metadata if we couldn't get diffs
|
|
1198
|
+
return {
|
|
1199
|
+
content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
1204
|
+
return {
|
|
1205
|
+
content: [{ type: "text", text: `Error getting pull request changes: ${errorMessage}` }],
|
|
1206
|
+
isError: true,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
server.tool(REPO_TOOLS.reply_to_comment, "Replies to a specific comment on a pull request.", {
|
|
1211
|
+
repositoryId: z
|
|
1212
|
+
.string()
|
|
1213
|
+
.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."),
|
|
1214
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request where the comment thread exists."),
|
|
1215
|
+
threadId: z.coerce.number().min(1).describe("The ID of the thread to which the comment will be added."),
|
|
1216
|
+
content: z.string().describe("The content of the comment to be added."),
|
|
1217
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
829
1218
|
fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of a simple confirmation message."),
|
|
830
1219
|
}, async ({ repositoryId, pullRequestId, threadId, content, project, fullResponse }) => {
|
|
831
1220
|
try {
|
|
@@ -857,17 +1246,23 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
857
1246
|
}
|
|
858
1247
|
});
|
|
859
1248
|
server.tool(REPO_TOOLS.create_pull_request_thread, "Creates a new comment thread on a pull request.", {
|
|
860
|
-
repositoryId: z
|
|
861
|
-
|
|
1249
|
+
repositoryId: z
|
|
1250
|
+
.string()
|
|
1251
|
+
.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."),
|
|
1252
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request where the comment thread exists."),
|
|
862
1253
|
content: z.string().describe("The content of the comment to be added."),
|
|
863
|
-
project: z.string().optional().describe("Project ID or project name
|
|
1254
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
864
1255
|
filePath: z.string().optional().describe("The path of the file where the comment thread will be created. (optional)"),
|
|
865
1256
|
status: z
|
|
866
1257
|
.enum(getEnumKeys(CommentThreadStatus))
|
|
867
1258
|
.optional()
|
|
868
1259
|
.default(CommentThreadStatus[CommentThreadStatus.Active])
|
|
869
1260
|
.describe("The status of the comment thread. Defaults to 'Active'."),
|
|
870
|
-
rightFileStartLine: z.
|
|
1261
|
+
rightFileStartLine: z.coerce
|
|
1262
|
+
.number()
|
|
1263
|
+
.min(1)
|
|
1264
|
+
.optional()
|
|
1265
|
+
.describe("Position of first character of the thread's span in right file. The line number of a thread's position. Starts at 1. (optional)"),
|
|
871
1266
|
rightFileStartOffset: z
|
|
872
1267
|
.number()
|
|
873
1268
|
.optional()
|
|
@@ -971,10 +1366,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
971
1366
|
}
|
|
972
1367
|
});
|
|
973
1368
|
server.tool(REPO_TOOLS.update_pull_request_thread, "Updates an existing comment thread on a pull request.", {
|
|
974
|
-
repositoryId: z
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1369
|
+
repositoryId: z
|
|
1370
|
+
.string()
|
|
1371
|
+
.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."),
|
|
1372
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request where the comment thread exists."),
|
|
1373
|
+
threadId: z.coerce.number().min(1).describe("The ID of the thread to update."),
|
|
1374
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
978
1375
|
status: z
|
|
979
1376
|
.enum(getEnumKeys(CommentThreadStatus))
|
|
980
1377
|
.optional()
|
|
@@ -1026,8 +1423,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1026
1423
|
.optional()
|
|
1027
1424
|
.default(GitVersionType[GitVersionType.Branch])
|
|
1028
1425
|
.describe("The meaning of the version parameter, e.g., branch, tag or commit"),
|
|
1029
|
-
skip: z.number().optional().default(0).describe("Number of commits to skip"),
|
|
1030
|
-
top: z.number().optional().default(10).describe("Maximum number of commits to return"),
|
|
1426
|
+
skip: z.coerce.number().optional().default(0).describe("Number of commits to skip"),
|
|
1427
|
+
top: z.coerce.number().optional().default(10).describe("Maximum number of commits to return"),
|
|
1031
1428
|
includeLinks: z.boolean().optional().default(false).describe("Include commit links"),
|
|
1032
1429
|
includeWorkItems: z.boolean().optional().default(false).describe("Include associated work items"),
|
|
1033
1430
|
// Enhanced search parameters
|
|
@@ -1182,10 +1579,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1182
1579
|
}
|
|
1183
1580
|
});
|
|
1184
1581
|
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."),
|
|
1186
|
-
pullRequestId: z.number().describe("The ID of the pull request."),
|
|
1582
|
+
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."),
|
|
1583
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request."),
|
|
1187
1584
|
vote: z.enum(["Approved", "ApprovedWithSuggestions", "NoVote", "WaitingForAuthor", "Rejected"]).describe("The vote to cast: Approved(10), Suggestions(5), None(0), Waiting(-5), Rejected(-10)."),
|
|
1188
|
-
|
|
1585
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
1586
|
+
}, async ({ repositoryId, pullRequestId, vote, project }) => {
|
|
1189
1587
|
const connection = await connectionProvider();
|
|
1190
1588
|
const gitApi = await connection.getGitApi();
|
|
1191
1589
|
const userDetails = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
|
|
@@ -1200,7 +1598,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1200
1598
|
WaitingForAuthor: -5,
|
|
1201
1599
|
Rejected: -10,
|
|
1202
1600
|
};
|
|
1203
|
-
await gitApi.createPullRequestReviewer({ vote: voteMap[vote], id: userId }, repositoryId, pullRequestId, userId);
|
|
1601
|
+
await gitApi.createPullRequestReviewer({ vote: voteMap[vote], id: userId }, repositoryId, pullRequestId, userId, project);
|
|
1204
1602
|
return {
|
|
1205
1603
|
content: [
|
|
1206
1604
|
{
|
|
@@ -1217,7 +1615,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1217
1615
|
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."),
|
|
1218
1616
|
versionType: z.enum(["Branch", "Commit", "Tag"]).optional().default("Branch").describe("The type of version identifier: 'Branch', 'Commit', or 'Tag'. Defaults to 'Branch'."),
|
|
1219
1617
|
recursive: z.boolean().optional().default(false).describe("Whether to list items recursively. Defaults to false."),
|
|
1220
|
-
recursionDepth: z.number().optional().default(1).describe("Maximum depth for recursive listing (1-10). Only applies when recursive is true. Defaults to 1."),
|
|
1618
|
+
recursionDepth: z.coerce.number().min(1).optional().default(1).describe("Maximum depth for recursive listing (1-10). Only applies when recursive is true. Defaults to 1."),
|
|
1221
1619
|
}, async ({ repositoryId, path, project, version, versionType, recursive, recursionDepth }) => {
|
|
1222
1620
|
try {
|
|
1223
1621
|
const connection = await connectionProvider();
|
|
@@ -1276,5 +1674,58 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1276
1674
|
};
|
|
1277
1675
|
}
|
|
1278
1676
|
});
|
|
1677
|
+
// ── Get file content at a specific version (branch, tag, or commit) ──
|
|
1678
|
+
const fileVersionTypeStrings = getEnumKeys(GitVersionType);
|
|
1679
|
+
server.tool(REPO_TOOLS.get_file_content, "Get the content of a file from a Git repository at a specific version (branch, tag, or commit SHA). " +
|
|
1680
|
+
"Useful for reading source files from PR branches, specific commits, or tags without having them checked out locally.", {
|
|
1681
|
+
repositoryId: z.string().describe("The ID (GUID) or name of the repository."),
|
|
1682
|
+
path: z.string().describe("The full path to the file in the repository, e.g., '/src/main.ts' or 'src/main.ts'."),
|
|
1683
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a name."),
|
|
1684
|
+
version: z
|
|
1685
|
+
.string()
|
|
1686
|
+
.optional()
|
|
1687
|
+
.describe("Version string: branch name (e.g. 'main'), tag name, or commit SHA. " + "Defaults to the repository's default branch if not specified."),
|
|
1688
|
+
versionType: z
|
|
1689
|
+
.enum(fileVersionTypeStrings)
|
|
1690
|
+
.optional()
|
|
1691
|
+
.default("Commit")
|
|
1692
|
+
.describe("How to interpret the 'version' parameter. Defaults to 'Commit'."),
|
|
1693
|
+
}, async ({ repositoryId, path, project, version, versionType }) => {
|
|
1694
|
+
try {
|
|
1695
|
+
const connection = await connectionProvider();
|
|
1696
|
+
const gitApi = await connection.getGitApi();
|
|
1697
|
+
// Build the version descriptor if a version was specified
|
|
1698
|
+
const versionDescriptor = version
|
|
1699
|
+
? {
|
|
1700
|
+
version: version,
|
|
1701
|
+
versionType: GitVersionType[versionType],
|
|
1702
|
+
}
|
|
1703
|
+
: undefined;
|
|
1704
|
+
// getItemText returns a ReadableStream of the file content as text
|
|
1705
|
+
const stream = await gitApi.getItemText(repositoryId, path, project, undefined, // scopePath
|
|
1706
|
+
undefined, // recursionLevel
|
|
1707
|
+
undefined, // includeContentMetadata
|
|
1708
|
+
undefined, // latestProcessedChange
|
|
1709
|
+
false, // download
|
|
1710
|
+
versionDescriptor, true // includeContent
|
|
1711
|
+
);
|
|
1712
|
+
const content = await streamToString(stream);
|
|
1713
|
+
return {
|
|
1714
|
+
content: [{ type: "text", text: content }],
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
catch (error) {
|
|
1718
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
1719
|
+
return {
|
|
1720
|
+
content: [
|
|
1721
|
+
{
|
|
1722
|
+
type: "text",
|
|
1723
|
+
text: `Error getting file content for '${path}': ${errorMessage}`,
|
|
1724
|
+
},
|
|
1725
|
+
],
|
|
1726
|
+
isError: true,
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1279
1730
|
}
|
|
1280
1731
|
export { REPO_TOOLS, configureRepoTools };
|