@azure-devops/mcp 2.7.0-nightly.20260518 → 2.7.0-nightly.20260520
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/dist/tools/repositories.js +197 -186
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -984,11 +984,14 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
984
984
|
originalPath: origPath,
|
|
985
985
|
};
|
|
986
986
|
});
|
|
987
|
-
|
|
988
|
-
|
|
987
|
+
try {
|
|
988
|
+
// Fetch diffs for modified files. Add/Delete files are excluded from getFileDiffs
|
|
989
|
+
// because they don't have two versions to compare; their content is fetched
|
|
990
|
+
// separately below via getItemText when includeLineContent is true.
|
|
991
|
+
let fileDiffs = [];
|
|
992
|
+
if (fileDiffParams.length > 0) {
|
|
989
993
|
// Azure DevOps getFileDiffs API accepts max 10 files per request
|
|
990
994
|
const FILE_DIFF_BATCH_SIZE = 10;
|
|
991
|
-
let fileDiffs = [];
|
|
992
995
|
for (let i = 0; i < fileDiffParams.length; i += FILE_DIFF_BATCH_SIZE) {
|
|
993
996
|
const batch = fileDiffParams.slice(i, i + FILE_DIFF_BATCH_SIZE);
|
|
994
997
|
const batchDiffs = await gitApi.getFileDiffs({
|
|
@@ -998,207 +1001,215 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
998
1001
|
}, project || "", repositoryId);
|
|
999
1002
|
fileDiffs = fileDiffs.concat(batchDiffs);
|
|
1000
1003
|
}
|
|
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
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
originalLineNumberStart: 0,
|
|
1046
|
-
originalLinesCount: 0,
|
|
1047
|
-
modifiedLineNumberStart: 1,
|
|
1048
|
-
modifiedLinesCount: targetLines.length,
|
|
1049
|
-
modifiedLines: targetLines,
|
|
1050
|
-
},
|
|
1051
|
-
],
|
|
1052
|
-
},
|
|
1053
|
-
};
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
catch (addError) {
|
|
1004
|
+
}
|
|
1005
|
+
// Merge diff content with change metadata.
|
|
1006
|
+
// Added/deleted entries get diff: null here and are enriched below.
|
|
1007
|
+
const enrichedChanges = {
|
|
1008
|
+
...changes,
|
|
1009
|
+
changeEntries: changes.changeEntries.map((entry) => {
|
|
1010
|
+
// Normalize path for comparison (remove leading slash)
|
|
1011
|
+
const entryPath = entry.item?.path?.startsWith("/") ? entry.item.path.substring(1) : entry.item?.path;
|
|
1012
|
+
const matchingDiff = fileDiffs.find((diff) => diff.path === entryPath);
|
|
1013
|
+
return {
|
|
1014
|
+
...entry,
|
|
1015
|
+
diff: matchingDiff || null,
|
|
1016
|
+
};
|
|
1017
|
+
}),
|
|
1018
|
+
};
|
|
1019
|
+
// If includeLineContent is true, fetch actual file content with concurrency limit
|
|
1020
|
+
if (includeLineContent && enrichedChanges.changeEntries) {
|
|
1021
|
+
const CONCURRENCY_LIMIT = 10;
|
|
1022
|
+
const entriesWithContent = [...enrichedChanges.changeEntries];
|
|
1023
|
+
for (let i = 0; i < entriesWithContent.length; i += CONCURRENCY_LIMIT) {
|
|
1024
|
+
const batch = entriesWithContent.slice(i, i + CONCURRENCY_LIMIT);
|
|
1025
|
+
const batchResults = await Promise.all(batch.map(async (entry) => {
|
|
1026
|
+
const ct = entry.changeType ?? 0;
|
|
1027
|
+
const isAdd = !!(ct & VersionControlChangeType.Add);
|
|
1028
|
+
const isDelete = !!(ct & VersionControlChangeType.Delete);
|
|
1029
|
+
const entryPath = entry.item?.path ? (entry.item.path.startsWith("/") ? entry.item.path.substring(1) : entry.item.path) : undefined;
|
|
1030
|
+
// For deleted files ADO sets item.path to null and puts the path in originalPath only.
|
|
1031
|
+
// Normalise originalPath once and use it as the fallback throughout.
|
|
1032
|
+
const normalizedOriginalPath = entry.originalPath ? (entry.originalPath.startsWith("/") ? entry.originalPath.substring(1) : entry.originalPath) : undefined;
|
|
1033
|
+
// effectivePath is what we use as the "current" path for API calls / early-exit guard.
|
|
1034
|
+
// For additions/modifications it's item.path; for deletions it's originalPath.
|
|
1035
|
+
const effectivePath = entryPath ?? normalizedOriginalPath;
|
|
1036
|
+
if (!effectivePath) {
|
|
1037
|
+
return entry;
|
|
1038
|
+
}
|
|
1039
|
+
// Handle added files: fetch full content at target commit and create synthetic diff
|
|
1040
|
+
if (isAdd && !entry.diff) {
|
|
1041
|
+
try {
|
|
1042
|
+
const targetStream = await gitApi
|
|
1043
|
+
.getItemText(repositoryId, effectivePath, project, undefined, undefined, undefined, undefined, undefined, { version: targetCommitId, versionType: GitVersionType.Commit })
|
|
1044
|
+
.catch(() => null);
|
|
1045
|
+
if (targetStream) {
|
|
1046
|
+
const targetText = await streamToString(targetStream);
|
|
1047
|
+
const targetLines = targetText.split(/\r?\n/);
|
|
1057
1048
|
return {
|
|
1058
1049
|
...entry,
|
|
1059
|
-
|
|
1050
|
+
diff: {
|
|
1051
|
+
path: effectivePath,
|
|
1052
|
+
originalPath: null,
|
|
1053
|
+
lineDiffBlocks: [
|
|
1054
|
+
{
|
|
1055
|
+
changeType: 1, // Add
|
|
1056
|
+
originalLineNumberStart: 0,
|
|
1057
|
+
originalLinesCount: 0,
|
|
1058
|
+
modifiedLineNumberStart: 1,
|
|
1059
|
+
modifiedLinesCount: targetLines.length,
|
|
1060
|
+
modifiedLines: targetLines,
|
|
1061
|
+
},
|
|
1062
|
+
],
|
|
1063
|
+
},
|
|
1060
1064
|
};
|
|
1061
1065
|
}
|
|
1062
|
-
return entry;
|
|
1063
1066
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
originalLinesCount: baseLines.length,
|
|
1084
|
-
modifiedLineNumberStart: 0,
|
|
1085
|
-
modifiedLinesCount: 0,
|
|
1086
|
-
originalLines: baseLines,
|
|
1087
|
-
},
|
|
1088
|
-
],
|
|
1089
|
-
},
|
|
1090
|
-
};
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
catch (delError) {
|
|
1067
|
+
catch (addError) {
|
|
1068
|
+
return {
|
|
1069
|
+
...entry,
|
|
1070
|
+
_contentFetchError: `Failed to fetch added file content: ${addError instanceof Error ? addError.message : "Unknown error"}`,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
return entry;
|
|
1074
|
+
}
|
|
1075
|
+
// Handle deleted files: fetch full content at base commit and create synthetic diff.
|
|
1076
|
+
// basePath prefers originalPath (the pre-deletion path); falls back to effectivePath.
|
|
1077
|
+
if (isDelete && !entry.diff) {
|
|
1078
|
+
try {
|
|
1079
|
+
const basePath = normalizedOriginalPath ?? effectivePath;
|
|
1080
|
+
const baseStream = await gitApi
|
|
1081
|
+
.getItemText(repositoryId, basePath, project, undefined, undefined, undefined, undefined, undefined, { version: baseCommitId, versionType: GitVersionType.Commit })
|
|
1082
|
+
.catch(() => null);
|
|
1083
|
+
if (baseStream) {
|
|
1084
|
+
const baseText = await streamToString(baseStream);
|
|
1085
|
+
const baseLines = baseText.split(/\r?\n/);
|
|
1094
1086
|
return {
|
|
1095
1087
|
...entry,
|
|
1096
|
-
|
|
1088
|
+
diff: {
|
|
1089
|
+
path: null,
|
|
1090
|
+
originalPath: basePath,
|
|
1091
|
+
lineDiffBlocks: [
|
|
1092
|
+
{
|
|
1093
|
+
changeType: 2, // Delete
|
|
1094
|
+
originalLineNumberStart: 1,
|
|
1095
|
+
originalLinesCount: baseLines.length,
|
|
1096
|
+
modifiedLineNumberStart: 0,
|
|
1097
|
+
modifiedLinesCount: 0,
|
|
1098
|
+
originalLines: baseLines,
|
|
1099
|
+
},
|
|
1100
|
+
],
|
|
1101
|
+
},
|
|
1097
1102
|
};
|
|
1098
1103
|
}
|
|
1099
|
-
return entry;
|
|
1100
|
-
}
|
|
1101
|
-
// For modified/renamed files, skip if no diff blocks
|
|
1102
|
-
if (!entry.diff?.lineDiffBlocks || entry.diff.lineDiffBlocks.length === 0) {
|
|
1103
|
-
return entry;
|
|
1104
|
-
}
|
|
1105
|
-
// For renamed/moved files, the base version is at the original path
|
|
1106
|
-
const basePath = entry.originalPath ? (entry.originalPath.startsWith("/") ? entry.originalPath.substring(1) : entry.originalPath) : entryPath;
|
|
1107
|
-
try {
|
|
1108
|
-
// Fetch file content at both commits
|
|
1109
|
-
const [baseContent, targetContent] = await Promise.all([
|
|
1110
|
-
// Base version (original) - use basePath for renamed files
|
|
1111
|
-
gitApi
|
|
1112
|
-
.getItemText(repositoryId, basePath, project, undefined, undefined, undefined, undefined, undefined, { version: baseCommitId, versionType: GitVersionType.Commit })
|
|
1113
|
-
.catch(() => null),
|
|
1114
|
-
// Target version (modified)
|
|
1115
|
-
gitApi
|
|
1116
|
-
.getItemText(repositoryId, entryPath, project, undefined, undefined, undefined, undefined, undefined, { version: targetCommitId, versionType: GitVersionType.Commit })
|
|
1117
|
-
.catch(() => null),
|
|
1118
|
-
]);
|
|
1119
|
-
// Convert streams to text
|
|
1120
|
-
const baseText = baseContent ? await streamToString(baseContent) : "";
|
|
1121
|
-
const targetText = targetContent ? await streamToString(targetContent) : "";
|
|
1122
|
-
// Check if response is an Azure DevOps error (returned as JSON in the stream)
|
|
1123
|
-
const checkForApiError = (text, label) => {
|
|
1124
|
-
if (text.startsWith("{")) {
|
|
1125
|
-
try {
|
|
1126
|
-
const parsed = JSON.parse(text);
|
|
1127
|
-
if (parsed.$id && parsed.innerException !== undefined) {
|
|
1128
|
-
throw new Error(`Failed to fetch ${label} file content: ${parsed.message || text}`);
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
catch (e) {
|
|
1132
|
-
if (e instanceof Error && e.message.startsWith("Failed to fetch"))
|
|
1133
|
-
throw e;
|
|
1134
|
-
// Not valid JSON or not an error response — treat as legitimate content
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
};
|
|
1138
|
-
checkForApiError(baseText, "base");
|
|
1139
|
-
checkForApiError(targetText, "target");
|
|
1140
|
-
// Split into lines
|
|
1141
|
-
const baseLines = baseText.split(/\r?\n/);
|
|
1142
|
-
const targetLines = targetText.split(/\r?\n/);
|
|
1143
|
-
// Enrich each lineDiffBlock with actual line content
|
|
1144
|
-
const enrichedDiff = {
|
|
1145
|
-
...entry.diff,
|
|
1146
|
-
lineDiffBlocks: entry.diff.lineDiffBlocks?.map((block) => {
|
|
1147
|
-
const enrichedBlock = { ...block };
|
|
1148
|
-
// Add original (base) lines if they exist
|
|
1149
|
-
if (block.originalLineNumberStart && block.originalLinesCount) {
|
|
1150
|
-
const startIdx = block.originalLineNumberStart - 1;
|
|
1151
|
-
const endIdx = startIdx + block.originalLinesCount;
|
|
1152
|
-
enrichedBlock.originalLines = baseLines.slice(startIdx, endIdx);
|
|
1153
|
-
}
|
|
1154
|
-
// Add modified (target) lines if they exist
|
|
1155
|
-
if (block.modifiedLineNumberStart && block.modifiedLinesCount) {
|
|
1156
|
-
const startIdx = block.modifiedLineNumberStart - 1;
|
|
1157
|
-
const endIdx = startIdx + block.modifiedLinesCount;
|
|
1158
|
-
enrichedBlock.modifiedLines = targetLines.slice(startIdx, endIdx);
|
|
1159
|
-
}
|
|
1160
|
-
return enrichedBlock;
|
|
1161
|
-
}),
|
|
1162
|
-
};
|
|
1163
|
-
return {
|
|
1164
|
-
...entry,
|
|
1165
|
-
diff: enrichedDiff,
|
|
1166
|
-
};
|
|
1167
1104
|
}
|
|
1168
|
-
catch (
|
|
1169
|
-
// If content fetch fails, return entry with error
|
|
1105
|
+
catch (delError) {
|
|
1170
1106
|
return {
|
|
1171
1107
|
...entry,
|
|
1172
|
-
_contentFetchError: `Failed to fetch
|
|
1108
|
+
_contentFetchError: `Failed to fetch deleted file content: ${delError instanceof Error ? delError.message : "Unknown error"}`,
|
|
1173
1109
|
};
|
|
1174
1110
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1111
|
+
return entry;
|
|
1112
|
+
}
|
|
1113
|
+
// For modified/renamed files, skip if no diff blocks
|
|
1114
|
+
if (!entry.diff?.lineDiffBlocks || entry.diff.lineDiffBlocks.length === 0) {
|
|
1115
|
+
return entry;
|
|
1116
|
+
}
|
|
1117
|
+
// For renamed/moved files, the base version is at the original path
|
|
1118
|
+
const basePath = normalizedOriginalPath ?? effectivePath;
|
|
1119
|
+
try {
|
|
1120
|
+
// Fetch file content at both commits
|
|
1121
|
+
const [baseContent, targetContent] = await Promise.all([
|
|
1122
|
+
// Base version (original) - use basePath for renamed files
|
|
1123
|
+
gitApi
|
|
1124
|
+
.getItemText(repositoryId, basePath, project, undefined, undefined, undefined, undefined, undefined, { version: baseCommitId, versionType: GitVersionType.Commit })
|
|
1125
|
+
.catch(() => null),
|
|
1126
|
+
// Target version (modified)
|
|
1127
|
+
gitApi
|
|
1128
|
+
.getItemText(repositoryId, effectivePath, project, undefined, undefined, undefined, undefined, undefined, { version: targetCommitId, versionType: GitVersionType.Commit })
|
|
1129
|
+
.catch(() => null),
|
|
1130
|
+
]);
|
|
1131
|
+
// Convert streams to text
|
|
1132
|
+
const baseText = baseContent ? await streamToString(baseContent) : "";
|
|
1133
|
+
const targetText = targetContent ? await streamToString(targetContent) : "";
|
|
1134
|
+
// Check if response is an Azure DevOps error (returned as JSON in the stream)
|
|
1135
|
+
const checkForApiError = (text, label) => {
|
|
1136
|
+
if (text.startsWith("{")) {
|
|
1137
|
+
try {
|
|
1138
|
+
const parsed = JSON.parse(text);
|
|
1139
|
+
if (parsed.$id && parsed.innerException !== undefined) {
|
|
1140
|
+
throw new Error(`Failed to fetch ${label} file content: ${parsed.message || text}`);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
catch (e) {
|
|
1144
|
+
if (e instanceof Error && e.message.startsWith("Failed to fetch"))
|
|
1145
|
+
throw e;
|
|
1146
|
+
// Not valid JSON or not an error response — treat as legitimate content
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
checkForApiError(baseText, "base");
|
|
1151
|
+
checkForApiError(targetText, "target");
|
|
1152
|
+
// Split into lines
|
|
1153
|
+
const baseLines = baseText.split(/\r?\n/);
|
|
1154
|
+
const targetLines = targetText.split(/\r?\n/);
|
|
1155
|
+
// Enrich each lineDiffBlock with actual line content
|
|
1156
|
+
const enrichedDiff = {
|
|
1157
|
+
...entry.diff,
|
|
1158
|
+
lineDiffBlocks: entry.diff.lineDiffBlocks?.map((block) => {
|
|
1159
|
+
const enrichedBlock = { ...block };
|
|
1160
|
+
// Add original (base) lines if they exist
|
|
1161
|
+
if (block.originalLineNumberStart && block.originalLinesCount) {
|
|
1162
|
+
const startIdx = block.originalLineNumberStart - 1;
|
|
1163
|
+
const endIdx = startIdx + block.originalLinesCount;
|
|
1164
|
+
enrichedBlock.originalLines = baseLines.slice(startIdx, endIdx);
|
|
1165
|
+
}
|
|
1166
|
+
// Add modified (target) lines if they exist
|
|
1167
|
+
if (block.modifiedLineNumberStart && block.modifiedLinesCount) {
|
|
1168
|
+
const startIdx = block.modifiedLineNumberStart - 1;
|
|
1169
|
+
const endIdx = startIdx + block.modifiedLinesCount;
|
|
1170
|
+
enrichedBlock.modifiedLines = targetLines.slice(startIdx, endIdx);
|
|
1171
|
+
}
|
|
1172
|
+
return enrichedBlock;
|
|
1173
|
+
}),
|
|
1174
|
+
};
|
|
1175
|
+
return {
|
|
1176
|
+
...entry,
|
|
1177
|
+
diff: enrichedDiff,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
catch (contentError) {
|
|
1181
|
+
// If content fetch fails, return entry with error
|
|
1182
|
+
return {
|
|
1183
|
+
...entry,
|
|
1184
|
+
_contentFetchError: `Failed to fetch line content: ${contentError instanceof Error ? contentError.message : "Unknown error"}`,
|
|
1185
|
+
};
|
|
1179
1186
|
}
|
|
1187
|
+
}));
|
|
1188
|
+
// Write batch results back into the array
|
|
1189
|
+
for (let j = 0; j < batchResults.length; j++) {
|
|
1190
|
+
entriesWithContent[i + j] = batchResults[j];
|
|
1180
1191
|
}
|
|
1181
|
-
enrichedChanges.changeEntries = entriesWithContent;
|
|
1182
1192
|
}
|
|
1183
|
-
|
|
1184
|
-
content: [{ type: "text", text: JSON.stringify(enrichedChanges, null, 2) }],
|
|
1185
|
-
};
|
|
1186
|
-
}
|
|
1187
|
-
catch (diffError) {
|
|
1188
|
-
// If diff fetching fails, return metadata with error info
|
|
1189
|
-
return {
|
|
1190
|
-
content: [
|
|
1191
|
-
{
|
|
1192
|
-
type: "text",
|
|
1193
|
-
text: JSON.stringify({
|
|
1194
|
-
...changes,
|
|
1195
|
-
_diffError: `Failed to fetch diff content: ${diffError instanceof Error ? diffError.message : "Unknown error"}`,
|
|
1196
|
-
_note: "Returned metadata only",
|
|
1197
|
-
}, null, 2),
|
|
1198
|
-
},
|
|
1199
|
-
],
|
|
1200
|
-
};
|
|
1193
|
+
enrichedChanges.changeEntries = entriesWithContent;
|
|
1201
1194
|
}
|
|
1195
|
+
return {
|
|
1196
|
+
content: [{ type: "text", text: JSON.stringify(enrichedChanges, null, 2) }],
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
catch (diffError) {
|
|
1200
|
+
// If diff fetching fails, return metadata with error info
|
|
1201
|
+
return {
|
|
1202
|
+
content: [
|
|
1203
|
+
{
|
|
1204
|
+
type: "text",
|
|
1205
|
+
text: JSON.stringify({
|
|
1206
|
+
...changes,
|
|
1207
|
+
_diffError: `Failed to fetch diff content: ${diffError instanceof Error ? diffError.message : "Unknown error"}`,
|
|
1208
|
+
_note: "Returned metadata only",
|
|
1209
|
+
}, null, 2),
|
|
1210
|
+
},
|
|
1211
|
+
],
|
|
1212
|
+
};
|
|
1202
1213
|
}
|
|
1203
1214
|
}
|
|
1204
1215
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.7.0-nightly.
|
|
1
|
+
export const packageVersion = "2.7.0-nightly.20260520";
|