@azure-devops/mcp 2.7.0 → 2.8.0-nightly.20260624
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 +68 -18
- package/dist/auth.js +0 -0
- package/dist/index.js +1 -1
- package/dist/logger.js +0 -0
- package/dist/org-tenants.js +0 -0
- package/dist/prompts.js +0 -0
- package/dist/shared/elicitations.js +10 -0
- package/dist/tools/pipelines.js +31 -9
- package/dist/tools/repositories.js +248 -207
- package/dist/tools/test-plans.js +6 -3
- package/dist/tools/wiki.js +30 -6
- package/dist/tools/work-items.js +48 -8
- package/dist/tools/work.js +1 -1
- package/dist/tools.js +0 -0
- package/dist/useragent.js +0 -0
- package/dist/utils.js +54 -0
- package/dist/version.js +1 -1
- package/package.json +4 -4
- package/dist/tools/builds.js +0 -271
- package/dist/tools/releases.js +0 -97
|
@@ -3,7 +3,7 @@
|
|
|
3
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, streamToString } from "../utils.js";
|
|
6
|
+
import { extractAdoStreamError, 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",
|
|
@@ -91,11 +91,13 @@ function trimPullRequest(pr, includeDescription = false) {
|
|
|
91
91
|
if (!pr) {
|
|
92
92
|
return null;
|
|
93
93
|
}
|
|
94
|
+
const statusName = typeof pr.status === "number" ? (PullRequestStatus[pr.status] ?? "Unknown") : "Unknown";
|
|
94
95
|
return {
|
|
95
96
|
pullRequestId: pr.pullRequestId,
|
|
96
97
|
codeReviewId: pr.codeReviewId,
|
|
97
98
|
repository: pr.repository?.name,
|
|
98
99
|
status: pr.status,
|
|
100
|
+
statusName,
|
|
99
101
|
createdBy: {
|
|
100
102
|
displayName: pr.createdBy?.displayName,
|
|
101
103
|
uniqueName: pr.createdBy?.uniqueName,
|
|
@@ -144,6 +146,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
144
146
|
const connection = await connectionProvider();
|
|
145
147
|
const gitApi = await connection.getGitApi();
|
|
146
148
|
const workItemRefs = workItems ? workItems.split(" ").map((id) => ({ id: id.trim() })) : [];
|
|
149
|
+
const noDataErrorMessage = `Pull request creation returned no data and no matching PR was found. This often means repositoryId=\"${repositoryId}\" was not resolvable. ` +
|
|
150
|
+
"Try the repository GUID from repo_list_repos_by_project instead of the Project/RepoName slash format.";
|
|
147
151
|
const forkSource = forkSourceRepositoryId
|
|
148
152
|
? {
|
|
149
153
|
repository: {
|
|
@@ -170,14 +174,16 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
170
174
|
}
|
|
171
175
|
else {
|
|
172
176
|
return {
|
|
173
|
-
content: [{ type: "text", text:
|
|
177
|
+
content: [{ type: "text", text: noDataErrorMessage }],
|
|
178
|
+
isError: true,
|
|
174
179
|
};
|
|
175
180
|
}
|
|
176
181
|
}
|
|
177
182
|
const trimmedPullRequest = trimPullRequest(pullRequest, true);
|
|
178
183
|
if (!trimmedPullRequest) {
|
|
179
184
|
return {
|
|
180
|
-
content: [{ type: "text", text:
|
|
185
|
+
content: [{ type: "text", text: noDataErrorMessage }],
|
|
186
|
+
isError: true,
|
|
181
187
|
};
|
|
182
188
|
}
|
|
183
189
|
return {
|
|
@@ -303,11 +309,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
303
309
|
.enum(getEnumKeys(GitPullRequestMergeStrategy))
|
|
304
310
|
.optional()
|
|
305
311
|
.describe("The merge strategy to use when the pull request autocompletes. Defaults to 'NoFastForward'."),
|
|
312
|
+
mergeCommitMessage: z.string().optional().describe("Commit message to use when the pull request is completed."),
|
|
306
313
|
deleteSourceBranch: z.boolean().optional().default(false).describe("Whether to delete the source branch when the pull request autocompletes. Defaults to false."),
|
|
307
314
|
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."),
|
|
308
315
|
bypassReason: z.string().optional().describe("Reason for bypassing branch policies. When provided, branch policies will be automatically bypassed during autocompletion."),
|
|
309
316
|
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."),
|
|
310
|
-
}, async ({ repositoryId, pullRequestId, project, title, description, isDraft, targetRefName, status, autoComplete, mergeStrategy, deleteSourceBranch, transitionWorkItems, bypassReason, labels, }) => {
|
|
317
|
+
}, async ({ repositoryId, pullRequestId, project, title, description, isDraft, targetRefName, status, autoComplete, mergeStrategy, mergeCommitMessage, deleteSourceBranch, transitionWorkItems, bypassReason, labels, }) => {
|
|
311
318
|
try {
|
|
312
319
|
const connection = await connectionProvider();
|
|
313
320
|
const gitApi = await connection.getGitApi();
|
|
@@ -337,6 +344,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
337
344
|
if (mergeStrategy) {
|
|
338
345
|
completionOptions.mergeStrategy = GitPullRequestMergeStrategy[mergeStrategy];
|
|
339
346
|
}
|
|
347
|
+
if (mergeCommitMessage) {
|
|
348
|
+
completionOptions.mergeCommitMessage = mergeCommitMessage;
|
|
349
|
+
}
|
|
340
350
|
if (bypassReason) {
|
|
341
351
|
completionOptions.bypassReason = bypassReason;
|
|
342
352
|
}
|
|
@@ -618,33 +628,33 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
618
628
|
try {
|
|
619
629
|
const connection = await connectionProvider();
|
|
620
630
|
const gitApi = await connection.getGitApi();
|
|
621
|
-
const threads = await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration);
|
|
631
|
+
const threads = (await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration)) ?? [];
|
|
622
632
|
let filteredThreads = threads;
|
|
623
633
|
if (status !== undefined) {
|
|
624
634
|
const statusValue = CommentThreadStatus[status];
|
|
625
|
-
filteredThreads = filteredThreads
|
|
635
|
+
filteredThreads = filteredThreads.filter((thread) => thread.status === statusValue);
|
|
626
636
|
}
|
|
627
637
|
if (authorEmail !== undefined) {
|
|
628
|
-
filteredThreads = filteredThreads
|
|
638
|
+
filteredThreads = filteredThreads.filter((thread) => {
|
|
629
639
|
const firstComment = thread.comments?.[0];
|
|
630
640
|
return firstComment?.author?.uniqueName?.toLowerCase() === authorEmail.toLowerCase();
|
|
631
641
|
});
|
|
632
642
|
}
|
|
633
643
|
if (authorDisplayName !== undefined) {
|
|
634
644
|
const lowerAuthorName = authorDisplayName.toLowerCase();
|
|
635
|
-
filteredThreads = filteredThreads
|
|
645
|
+
filteredThreads = filteredThreads.filter((thread) => {
|
|
636
646
|
const firstComment = thread.comments?.[0];
|
|
637
647
|
return firstComment?.author?.displayName?.toLowerCase().includes(lowerAuthorName);
|
|
638
648
|
});
|
|
639
649
|
}
|
|
640
|
-
const paginatedThreads = filteredThreads
|
|
650
|
+
const paginatedThreads = filteredThreads.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
|
|
641
651
|
if (fullResponse) {
|
|
642
652
|
return {
|
|
643
653
|
content: [{ type: "text", text: JSON.stringify(paginatedThreads, null, 2) }],
|
|
644
654
|
};
|
|
645
655
|
}
|
|
646
656
|
// Return trimmed thread data focusing on essential information
|
|
647
|
-
const trimmedThreads = paginatedThreads
|
|
657
|
+
const trimmedThreads = paginatedThreads.map((thread) => trimPullRequestThread(thread));
|
|
648
658
|
return {
|
|
649
659
|
content: [{ type: "text", text: JSON.stringify(trimmedThreads, null, 2) }],
|
|
650
660
|
};
|
|
@@ -693,7 +703,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
693
703
|
};
|
|
694
704
|
}
|
|
695
705
|
});
|
|
696
|
-
server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of
|
|
706
|
+
server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branch names for a given repository. Returns an array of branch name strings, not full branch objects. Use repo_get_branch_by_name to get full details for a specific branch.", {
|
|
697
707
|
repositoryId: z
|
|
698
708
|
.string()
|
|
699
709
|
.describe("The ID or name of the repository where the branches are located. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
@@ -718,7 +728,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
718
728
|
};
|
|
719
729
|
}
|
|
720
730
|
});
|
|
721
|
-
server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my
|
|
731
|
+
server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branch names for a given repository Id. Returns an array of branch name strings, not full branch objects. Use repo_get_branch_by_name to get full details for a specific branch.", {
|
|
722
732
|
repositoryId: z
|
|
723
733
|
.string()
|
|
724
734
|
.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."),
|
|
@@ -770,7 +780,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
770
780
|
};
|
|
771
781
|
}
|
|
772
782
|
});
|
|
773
|
-
server.tool(REPO_TOOLS.get_branch_by_name, "Get a branch by its name.", {
|
|
783
|
+
server.tool(REPO_TOOLS.get_branch_by_name, "Get a branch by its name. Returns isError: true if the branch is not found.", {
|
|
774
784
|
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."),
|
|
775
785
|
branchName: z.string().describe("The name of the branch to retrieve, e.g., 'main' or 'feature-branch'."),
|
|
776
786
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
@@ -976,11 +986,14 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
976
986
|
originalPath: origPath,
|
|
977
987
|
};
|
|
978
988
|
});
|
|
979
|
-
|
|
980
|
-
|
|
989
|
+
try {
|
|
990
|
+
// Fetch diffs for modified files. Add/Delete files are excluded from getFileDiffs
|
|
991
|
+
// because they don't have two versions to compare; their content is fetched
|
|
992
|
+
// separately below via getItemText when includeLineContent is true.
|
|
993
|
+
let fileDiffs = [];
|
|
994
|
+
if (fileDiffParams.length > 0) {
|
|
981
995
|
// Azure DevOps getFileDiffs API accepts max 10 files per request
|
|
982
996
|
const FILE_DIFF_BATCH_SIZE = 10;
|
|
983
|
-
let fileDiffs = [];
|
|
984
997
|
for (let i = 0; i < fileDiffParams.length; i += FILE_DIFF_BATCH_SIZE) {
|
|
985
998
|
const batch = fileDiffParams.slice(i, i + FILE_DIFF_BATCH_SIZE);
|
|
986
999
|
const batchDiffs = await gitApi.getFileDiffs({
|
|
@@ -990,207 +1003,215 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
990
1003
|
}, project || "", repositoryId);
|
|
991
1004
|
fileDiffs = fileDiffs.concat(batchDiffs);
|
|
992
1005
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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) {
|
|
1006
|
+
}
|
|
1007
|
+
// Merge diff content with change metadata.
|
|
1008
|
+
// Added/deleted entries get diff: null here and are enriched below.
|
|
1009
|
+
const enrichedChanges = {
|
|
1010
|
+
...changes,
|
|
1011
|
+
changeEntries: changes.changeEntries.map((entry) => {
|
|
1012
|
+
// Normalize path for comparison (remove leading slash)
|
|
1013
|
+
const entryPath = entry.item?.path?.startsWith("/") ? entry.item.path.substring(1) : entry.item?.path;
|
|
1014
|
+
const matchingDiff = fileDiffs.find((diff) => diff.path === entryPath);
|
|
1015
|
+
return {
|
|
1016
|
+
...entry,
|
|
1017
|
+
diff: matchingDiff || null,
|
|
1018
|
+
};
|
|
1019
|
+
}),
|
|
1020
|
+
};
|
|
1021
|
+
// If includeLineContent is true, fetch actual file content with concurrency limit
|
|
1022
|
+
if (includeLineContent && enrichedChanges.changeEntries) {
|
|
1023
|
+
const CONCURRENCY_LIMIT = 10;
|
|
1024
|
+
const entriesWithContent = [...enrichedChanges.changeEntries];
|
|
1025
|
+
for (let i = 0; i < entriesWithContent.length; i += CONCURRENCY_LIMIT) {
|
|
1026
|
+
const batch = entriesWithContent.slice(i, i + CONCURRENCY_LIMIT);
|
|
1027
|
+
const batchResults = await Promise.all(batch.map(async (entry) => {
|
|
1028
|
+
const ct = entry.changeType ?? 0;
|
|
1029
|
+
const isAdd = !!(ct & VersionControlChangeType.Add);
|
|
1030
|
+
const isDelete = !!(ct & VersionControlChangeType.Delete);
|
|
1031
|
+
const entryPath = entry.item?.path ? (entry.item.path.startsWith("/") ? entry.item.path.substring(1) : entry.item.path) : undefined;
|
|
1032
|
+
// For deleted files ADO sets item.path to null and puts the path in originalPath only.
|
|
1033
|
+
// Normalise originalPath once and use it as the fallback throughout.
|
|
1034
|
+
const normalizedOriginalPath = entry.originalPath ? (entry.originalPath.startsWith("/") ? entry.originalPath.substring(1) : entry.originalPath) : undefined;
|
|
1035
|
+
// effectivePath is what we use as the "current" path for API calls / early-exit guard.
|
|
1036
|
+
// For additions/modifications it's item.path; for deletions it's originalPath.
|
|
1037
|
+
const effectivePath = entryPath ?? normalizedOriginalPath;
|
|
1038
|
+
if (!effectivePath) {
|
|
1039
|
+
return entry;
|
|
1040
|
+
}
|
|
1041
|
+
// Handle added files: fetch full content at target commit and create synthetic diff
|
|
1042
|
+
if (isAdd && !entry.diff) {
|
|
1043
|
+
try {
|
|
1044
|
+
const targetStream = await gitApi
|
|
1045
|
+
.getItemText(repositoryId, effectivePath, project, undefined, undefined, undefined, undefined, undefined, { version: targetCommitId, versionType: GitVersionType.Commit })
|
|
1046
|
+
.catch(() => null);
|
|
1047
|
+
if (targetStream) {
|
|
1048
|
+
const targetText = await streamToString(targetStream);
|
|
1049
|
+
const targetLines = targetText.split(/\r?\n/);
|
|
1049
1050
|
return {
|
|
1050
1051
|
...entry,
|
|
1051
|
-
|
|
1052
|
+
diff: {
|
|
1053
|
+
path: effectivePath,
|
|
1054
|
+
originalPath: null,
|
|
1055
|
+
lineDiffBlocks: [
|
|
1056
|
+
{
|
|
1057
|
+
changeType: 1, // Add
|
|
1058
|
+
originalLineNumberStart: 0,
|
|
1059
|
+
originalLinesCount: 0,
|
|
1060
|
+
modifiedLineNumberStart: 1,
|
|
1061
|
+
modifiedLinesCount: targetLines.length,
|
|
1062
|
+
modifiedLines: targetLines,
|
|
1063
|
+
},
|
|
1064
|
+
],
|
|
1065
|
+
},
|
|
1052
1066
|
};
|
|
1053
1067
|
}
|
|
1054
|
-
return entry;
|
|
1055
1068
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
originalLinesCount: baseLines.length,
|
|
1076
|
-
modifiedLineNumberStart: 0,
|
|
1077
|
-
modifiedLinesCount: 0,
|
|
1078
|
-
originalLines: baseLines,
|
|
1079
|
-
},
|
|
1080
|
-
],
|
|
1081
|
-
},
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
catch (delError) {
|
|
1069
|
+
catch (addError) {
|
|
1070
|
+
return {
|
|
1071
|
+
...entry,
|
|
1072
|
+
_contentFetchError: `Failed to fetch added file content: ${addError instanceof Error ? addError.message : "Unknown error"}`,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
return entry;
|
|
1076
|
+
}
|
|
1077
|
+
// Handle deleted files: fetch full content at base commit and create synthetic diff.
|
|
1078
|
+
// basePath prefers originalPath (the pre-deletion path); falls back to effectivePath.
|
|
1079
|
+
if (isDelete && !entry.diff) {
|
|
1080
|
+
try {
|
|
1081
|
+
const basePath = normalizedOriginalPath ?? effectivePath;
|
|
1082
|
+
const baseStream = await gitApi
|
|
1083
|
+
.getItemText(repositoryId, basePath, project, undefined, undefined, undefined, undefined, undefined, { version: baseCommitId, versionType: GitVersionType.Commit })
|
|
1084
|
+
.catch(() => null);
|
|
1085
|
+
if (baseStream) {
|
|
1086
|
+
const baseText = await streamToString(baseStream);
|
|
1087
|
+
const baseLines = baseText.split(/\r?\n/);
|
|
1086
1088
|
return {
|
|
1087
1089
|
...entry,
|
|
1088
|
-
|
|
1090
|
+
diff: {
|
|
1091
|
+
path: null,
|
|
1092
|
+
originalPath: basePath,
|
|
1093
|
+
lineDiffBlocks: [
|
|
1094
|
+
{
|
|
1095
|
+
changeType: 2, // Delete
|
|
1096
|
+
originalLineNumberStart: 1,
|
|
1097
|
+
originalLinesCount: baseLines.length,
|
|
1098
|
+
modifiedLineNumberStart: 0,
|
|
1099
|
+
modifiedLinesCount: 0,
|
|
1100
|
+
originalLines: baseLines,
|
|
1101
|
+
},
|
|
1102
|
+
],
|
|
1103
|
+
},
|
|
1089
1104
|
};
|
|
1090
1105
|
}
|
|
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
1106
|
}
|
|
1160
|
-
catch (
|
|
1161
|
-
// If content fetch fails, return entry with error
|
|
1107
|
+
catch (delError) {
|
|
1162
1108
|
return {
|
|
1163
1109
|
...entry,
|
|
1164
|
-
_contentFetchError: `Failed to fetch
|
|
1110
|
+
_contentFetchError: `Failed to fetch deleted file content: ${delError instanceof Error ? delError.message : "Unknown error"}`,
|
|
1165
1111
|
};
|
|
1166
1112
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1113
|
+
return entry;
|
|
1114
|
+
}
|
|
1115
|
+
// For modified/renamed files, skip if no diff blocks
|
|
1116
|
+
if (!entry.diff?.lineDiffBlocks || entry.diff.lineDiffBlocks.length === 0) {
|
|
1117
|
+
return entry;
|
|
1118
|
+
}
|
|
1119
|
+
// For renamed/moved files, the base version is at the original path
|
|
1120
|
+
const basePath = normalizedOriginalPath ?? effectivePath;
|
|
1121
|
+
try {
|
|
1122
|
+
// Fetch file content at both commits
|
|
1123
|
+
const [baseContent, targetContent] = await Promise.all([
|
|
1124
|
+
// Base version (original) - use basePath for renamed files
|
|
1125
|
+
gitApi
|
|
1126
|
+
.getItemText(repositoryId, basePath, project, undefined, undefined, undefined, undefined, undefined, { version: baseCommitId, versionType: GitVersionType.Commit })
|
|
1127
|
+
.catch(() => null),
|
|
1128
|
+
// Target version (modified)
|
|
1129
|
+
gitApi
|
|
1130
|
+
.getItemText(repositoryId, effectivePath, project, undefined, undefined, undefined, undefined, undefined, { version: targetCommitId, versionType: GitVersionType.Commit })
|
|
1131
|
+
.catch(() => null),
|
|
1132
|
+
]);
|
|
1133
|
+
// Convert streams to text
|
|
1134
|
+
const baseText = baseContent ? await streamToString(baseContent) : "";
|
|
1135
|
+
const targetText = targetContent ? await streamToString(targetContent) : "";
|
|
1136
|
+
// Check if response is an Azure DevOps error (returned as JSON in the stream)
|
|
1137
|
+
const checkForApiError = (text, label) => {
|
|
1138
|
+
if (text.startsWith("{")) {
|
|
1139
|
+
try {
|
|
1140
|
+
const parsed = JSON.parse(text);
|
|
1141
|
+
if (parsed.$id && parsed.innerException !== undefined) {
|
|
1142
|
+
throw new Error(`Failed to fetch ${label} file content: ${parsed.message || text}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
catch (e) {
|
|
1146
|
+
if (e instanceof Error && e.message.startsWith("Failed to fetch"))
|
|
1147
|
+
throw e;
|
|
1148
|
+
// Not valid JSON or not an error response — treat as legitimate content
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
checkForApiError(baseText, "base");
|
|
1153
|
+
checkForApiError(targetText, "target");
|
|
1154
|
+
// Split into lines
|
|
1155
|
+
const baseLines = baseText.split(/\r?\n/);
|
|
1156
|
+
const targetLines = targetText.split(/\r?\n/);
|
|
1157
|
+
// Enrich each lineDiffBlock with actual line content
|
|
1158
|
+
const enrichedDiff = {
|
|
1159
|
+
...entry.diff,
|
|
1160
|
+
lineDiffBlocks: entry.diff.lineDiffBlocks?.map((block) => {
|
|
1161
|
+
const enrichedBlock = { ...block };
|
|
1162
|
+
// Add original (base) lines if they exist
|
|
1163
|
+
if (block.originalLineNumberStart && block.originalLinesCount) {
|
|
1164
|
+
const startIdx = block.originalLineNumberStart - 1;
|
|
1165
|
+
const endIdx = startIdx + block.originalLinesCount;
|
|
1166
|
+
enrichedBlock.originalLines = baseLines.slice(startIdx, endIdx);
|
|
1167
|
+
}
|
|
1168
|
+
// Add modified (target) lines if they exist
|
|
1169
|
+
if (block.modifiedLineNumberStart && block.modifiedLinesCount) {
|
|
1170
|
+
const startIdx = block.modifiedLineNumberStart - 1;
|
|
1171
|
+
const endIdx = startIdx + block.modifiedLinesCount;
|
|
1172
|
+
enrichedBlock.modifiedLines = targetLines.slice(startIdx, endIdx);
|
|
1173
|
+
}
|
|
1174
|
+
return enrichedBlock;
|
|
1175
|
+
}),
|
|
1176
|
+
};
|
|
1177
|
+
return {
|
|
1178
|
+
...entry,
|
|
1179
|
+
diff: enrichedDiff,
|
|
1180
|
+
};
|
|
1171
1181
|
}
|
|
1182
|
+
catch (contentError) {
|
|
1183
|
+
// If content fetch fails, return entry with error
|
|
1184
|
+
return {
|
|
1185
|
+
...entry,
|
|
1186
|
+
_contentFetchError: `Failed to fetch line content: ${contentError instanceof Error ? contentError.message : "Unknown error"}`,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
}));
|
|
1190
|
+
// Write batch results back into the array
|
|
1191
|
+
for (let j = 0; j < batchResults.length; j++) {
|
|
1192
|
+
entriesWithContent[i + j] = batchResults[j];
|
|
1172
1193
|
}
|
|
1173
|
-
enrichedChanges.changeEntries = entriesWithContent;
|
|
1174
1194
|
}
|
|
1175
|
-
|
|
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
|
-
};
|
|
1195
|
+
enrichedChanges.changeEntries = entriesWithContent;
|
|
1193
1196
|
}
|
|
1197
|
+
return {
|
|
1198
|
+
content: [{ type: "text", text: JSON.stringify(enrichedChanges, null, 2) }],
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
catch (diffError) {
|
|
1202
|
+
// If diff fetching fails, return metadata with error info
|
|
1203
|
+
return {
|
|
1204
|
+
content: [
|
|
1205
|
+
{
|
|
1206
|
+
type: "text",
|
|
1207
|
+
text: JSON.stringify({
|
|
1208
|
+
...changes,
|
|
1209
|
+
_diffError: `Failed to fetch diff content: ${diffError instanceof Error ? diffError.message : "Unknown error"}`,
|
|
1210
|
+
_note: "Returned metadata only",
|
|
1211
|
+
}, null, 2),
|
|
1212
|
+
},
|
|
1213
|
+
],
|
|
1214
|
+
};
|
|
1194
1215
|
}
|
|
1195
1216
|
}
|
|
1196
1217
|
}
|
|
@@ -1220,7 +1241,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1220
1241
|
try {
|
|
1221
1242
|
const connection = await connectionProvider();
|
|
1222
1243
|
const gitApi = await connection.getGitApi();
|
|
1223
|
-
const comment = await gitApi.createComment({ content }, repositoryId, pullRequestId, threadId, project);
|
|
1244
|
+
const comment = await gitApi.createComment({ content, commentType: 1 }, repositoryId, pullRequestId, threadId, project);
|
|
1224
1245
|
// Check if the comment was successfully created
|
|
1225
1246
|
if (!comment) {
|
|
1226
1247
|
return {
|
|
@@ -1266,7 +1287,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1266
1287
|
rightFileStartOffset: z
|
|
1267
1288
|
.number()
|
|
1268
1289
|
.optional()
|
|
1269
|
-
.describe("
|
|
1290
|
+
.describe("Start character offset of the thread's span within the line in the right file. The character offset of a thread's position inside of a line. Starts at 1. Must be set if rightFileStartLine is also specified. (optional)"),
|
|
1270
1291
|
rightFileEndLine: z
|
|
1271
1292
|
.number()
|
|
1272
1293
|
.optional()
|
|
@@ -1274,7 +1295,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1274
1295
|
rightFileEndOffset: z
|
|
1275
1296
|
.number()
|
|
1276
1297
|
.optional()
|
|
1277
|
-
.describe("
|
|
1298
|
+
.describe("Exclusive end character offset of the thread's span within the line in the right file. This value is exclusive: to cover the entire line, set it to (length of the original line text) + 1. When posting a suggestion, always calculate this from the existing file content being replaced, not from the suggestion or replacement text. Must be set if rightFileEndLine is also specified. (optional)"),
|
|
1278
1299
|
}, async ({ repositoryId, pullRequestId, content, project, filePath, status, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset }) => {
|
|
1279
1300
|
try {
|
|
1280
1301
|
const connection = await connectionProvider();
|
|
@@ -1351,7 +1372,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1351
1372
|
};
|
|
1352
1373
|
}
|
|
1353
1374
|
}
|
|
1354
|
-
const thread = await gitApi.createThread({ comments: [{ content: content }], threadContext: threadContext, status: CommentThreadStatus[status] }, repositoryId, pullRequestId, project);
|
|
1375
|
+
const thread = await gitApi.createThread({ comments: [{ content: content, commentType: 1 }], threadContext: threadContext, status: CommentThreadStatus[status] }, repositoryId, pullRequestId, project);
|
|
1355
1376
|
const trimmedThread = trimPullRequestThread(thread);
|
|
1356
1377
|
return {
|
|
1357
1378
|
content: [{ type: "text", text: JSON.stringify(trimmedThread, null, 2) }],
|
|
@@ -1598,7 +1619,18 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1598
1619
|
WaitingForAuthor: -5,
|
|
1599
1620
|
Rejected: -10,
|
|
1600
1621
|
};
|
|
1601
|
-
await gitApi.
|
|
1622
|
+
const existingReviewer = await gitApi.getPullRequestReviewer(repositoryId, pullRequestId, userId, project).catch((error) => {
|
|
1623
|
+
if (!(error instanceof Error) || !/not found|reviewer does not exist/i.test(error.message)) {
|
|
1624
|
+
throw error;
|
|
1625
|
+
}
|
|
1626
|
+
return undefined;
|
|
1627
|
+
});
|
|
1628
|
+
const reviewerPayload = {
|
|
1629
|
+
vote: voteMap[vote],
|
|
1630
|
+
id: userId,
|
|
1631
|
+
...(existingReviewer?.isRequired !== undefined ? { isRequired: existingReviewer.isRequired } : {}),
|
|
1632
|
+
};
|
|
1633
|
+
await gitApi.createPullRequestReviewer(reviewerPayload, repositoryId, pullRequestId, userId, project);
|
|
1602
1634
|
return {
|
|
1603
1635
|
content: [
|
|
1604
1636
|
{
|
|
@@ -1608,7 +1640,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1608
1640
|
],
|
|
1609
1641
|
};
|
|
1610
1642
|
});
|
|
1611
|
-
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.", {
|
|
1643
|
+
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. Returns isError: true if the path is not found.", {
|
|
1612
1644
|
repositoryId: z.string().describe("The ID or name of the repository."),
|
|
1613
1645
|
path: z.string().optional().default("/").describe("The directory path to list (e.g., '/src' or '/src/components'). Defaults to repository root."),
|
|
1614
1646
|
project: z.string().optional().describe("Project ID or name. Required if repositoryId is a name rather than a GUID."),
|
|
@@ -1629,7 +1661,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1629
1661
|
const items = await gitApi.getItems(repositoryId, project, path, recursionType, true, false, false, false, versionDescriptor);
|
|
1630
1662
|
if (!items || items.length === 0) {
|
|
1631
1663
|
return {
|
|
1632
|
-
content: [{ type: "text", text: `No items found at path: ${path}
|
|
1664
|
+
content: [{ type: "text", text: `No items found at path: ${path}. The path may not exist in the repository.` }],
|
|
1665
|
+
isError: true,
|
|
1633
1666
|
};
|
|
1634
1667
|
}
|
|
1635
1668
|
let filteredItems = items;
|
|
@@ -1677,7 +1710,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1677
1710
|
// ── Get file content at a specific version (branch, tag, or commit) ──
|
|
1678
1711
|
const fileVersionTypeStrings = getEnumKeys(GitVersionType);
|
|
1679
1712
|
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."
|
|
1713
|
+
"Useful for reading source files from PR branches, specific commits, or tags without having them checked out locally. " +
|
|
1714
|
+
"Returns isError: true if the file is not found.", {
|
|
1681
1715
|
repositoryId: z.string().describe("The ID (GUID) or name of the repository."),
|
|
1682
1716
|
path: z.string().describe("The full path to the file in the repository, e.g., '/src/main.ts' or 'src/main.ts'."),
|
|
1683
1717
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a name."),
|
|
@@ -1710,6 +1744,13 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1710
1744
|
versionDescriptor, true // includeContent
|
|
1711
1745
|
);
|
|
1712
1746
|
const content = await streamToString(stream);
|
|
1747
|
+
const streamError = extractAdoStreamError(content);
|
|
1748
|
+
if (streamError) {
|
|
1749
|
+
return {
|
|
1750
|
+
content: [{ type: "text", text: `Error getting file content for '${path}': ${streamError}` }],
|
|
1751
|
+
isError: true,
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1713
1754
|
return {
|
|
1714
1755
|
content: [{ type: "text", text: content }],
|
|
1715
1756
|
};
|