@mcp-consultant-tools/azure-devops 27.0.0-beta.1 → 27.0.0-beta.2
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/build/index.d.ts.map +1 -1
- package/build/index.js +528 -1
- package/build/index.js.map +1 -1
- package/build/sync/file-utils.d.ts +62 -0
- package/build/sync/file-utils.d.ts.map +1 -0
- package/build/sync/file-utils.js +150 -0
- package/build/sync/file-utils.js.map +1 -0
- package/build/sync/git-utils.d.ts +31 -0
- package/build/sync/git-utils.d.ts.map +1 -0
- package/build/sync/git-utils.js +116 -0
- package/build/sync/git-utils.js.map +1 -0
- package/build/sync/html-detection.d.ts +83 -0
- package/build/sync/html-detection.d.ts.map +1 -0
- package/build/sync/html-detection.js +146 -0
- package/build/sync/html-detection.js.map +1 -0
- package/build/sync/index.d.ts +11 -0
- package/build/sync/index.d.ts.map +1 -0
- package/build/sync/index.js +11 -0
- package/build/sync/index.js.map +1 -0
- package/build/sync/markdown-serializer.d.ts +82 -0
- package/build/sync/markdown-serializer.d.ts.map +1 -0
- package/build/sync/markdown-serializer.js +382 -0
- package/build/sync/markdown-serializer.js.map +1 -0
- package/build/sync/task-serializer.d.ts +92 -0
- package/build/sync/task-serializer.d.ts.map +1 -0
- package/build/sync/task-serializer.js +381 -0
- package/build/sync/task-serializer.js.map +1 -0
- package/package.json +1 -1
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;GAIG;AAMH,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;GAIG;AAMH,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAgC7D;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,GAAG,EAAE,kBAAkB,CAAC,EAAE,kBAAkB,QAgrD5F;AAED;;GAEG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
|
package/build/index.js
CHANGED
|
@@ -10,6 +10,9 @@ import { realpathSync } from "node:fs";
|
|
|
10
10
|
import { createMcpServer, createEnvLoader } from "@mcp-consultant-tools/core";
|
|
11
11
|
import { AzureDevOpsService } from "./AzureDevOpsService.js";
|
|
12
12
|
import { z } from 'zod';
|
|
13
|
+
import { checkFieldFormats, getConversionInstructions, workItemToMarkdown, commentsToMarkdown, buildPatchOperations, updateSyncRevision, getSyncConfig, ensureFolderExists, getWorkItemFilePath, getCommentsFilePath, fileExists, writeWorkItemFile, readWorkItemFile, readFileContent, listSyncedWorkItems, validateFolderPath, autoCommitMultipleFiles,
|
|
14
|
+
// Task sync utilities
|
|
15
|
+
tasksToMarkdown, parseTasksMarkdown, buildTaskPatchOperations, buildNewTaskFields, getTasksFilePath, updateTasksFileAfterCreate, } from './sync/index.js';
|
|
13
16
|
/**
|
|
14
17
|
* Register azure-devops tools and prompts to an MCP server
|
|
15
18
|
* @param server - The MCP server instance
|
|
@@ -316,6 +319,45 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
316
319
|
// ========================================
|
|
317
320
|
// TOOLS
|
|
318
321
|
// ========================================
|
|
322
|
+
server.tool("get-configuration", "Get the configured Azure DevOps organization and projects. Use this to construct correct URLs.", {}, async () => {
|
|
323
|
+
try {
|
|
324
|
+
const organization = process.env.AZUREDEVOPS_ORGANIZATION;
|
|
325
|
+
const projects = process.env.AZUREDEVOPS_PROJECTS?.split(",").map(p => p.trim()).filter(p => p) || [];
|
|
326
|
+
const syncFolder = process.env.AZUREDEVOPS_SYNC_FOLDER || 'docs/user-stories';
|
|
327
|
+
if (!organization || projects.length === 0) {
|
|
328
|
+
return {
|
|
329
|
+
content: [{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: "Azure DevOps not configured. Set AZUREDEVOPS_ORGANIZATION and AZUREDEVOPS_PROJECTS environment variables.",
|
|
332
|
+
}],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const config = {
|
|
336
|
+
organization,
|
|
337
|
+
projects,
|
|
338
|
+
syncFolder,
|
|
339
|
+
urlPatterns: {
|
|
340
|
+
workItem: `https://dev.azure.com/${organization}/{project}/_workitems/edit/{id}`,
|
|
341
|
+
pullRequest: `https://dev.azure.com/${organization}/{project}/_git/{repo}/pullrequest/{id}`,
|
|
342
|
+
wiki: `https://dev.azure.com/${organization}/{project}/_wiki/wikis/{wikiName}/{pagePath}`,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
return {
|
|
346
|
+
content: [{
|
|
347
|
+
type: "text",
|
|
348
|
+
text: `Azure DevOps Configuration:\n\n${JSON.stringify(config, null, 2)}`,
|
|
349
|
+
}],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
return {
|
|
354
|
+
content: [{
|
|
355
|
+
type: "text",
|
|
356
|
+
text: `Failed to get configuration: ${error.message}`,
|
|
357
|
+
}],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
});
|
|
319
361
|
server.tool("get-wikis", "Get all wikis in an Azure DevOps project", {
|
|
320
362
|
project: z.string().describe("The project name"),
|
|
321
363
|
}, async ({ project }) => {
|
|
@@ -900,8 +942,493 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
900
942
|
};
|
|
901
943
|
}
|
|
902
944
|
});
|
|
945
|
+
// ========================================
|
|
946
|
+
// WORK ITEM SYNC TOOLS (Local Markdown)
|
|
947
|
+
// ========================================
|
|
948
|
+
server.tool("sync-work-item-to-file", "Download work item(s) from ADO and save as local markdown file(s). Token-efficient for editing. Requires markdown format fields (not HTML).", {
|
|
949
|
+
project: z.string().describe("The project name"),
|
|
950
|
+
workItemIds: z.array(z.number()).describe("Work item IDs to pull"),
|
|
951
|
+
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
952
|
+
includeComments: z.boolean().optional().describe("Also save comments to {id}-comments.md (default: false)"),
|
|
953
|
+
}, async ({ project, workItemIds, folder, includeComments }) => {
|
|
954
|
+
try {
|
|
955
|
+
const service = getAzureDevOpsService();
|
|
956
|
+
const syncConfig = getSyncConfig(folder);
|
|
957
|
+
// Validate folder path for security
|
|
958
|
+
validateFolderPath(syncConfig.folder);
|
|
959
|
+
const pulled = [];
|
|
960
|
+
const skipped = [];
|
|
961
|
+
const commentsFiles = [];
|
|
962
|
+
const filesToCommit = [];
|
|
963
|
+
await ensureFolderExists(syncConfig.folder);
|
|
964
|
+
for (const workItemId of workItemIds) {
|
|
965
|
+
try {
|
|
966
|
+
// Fetch work item from ADO
|
|
967
|
+
const workItem = await service.getWorkItem(project, workItemId);
|
|
968
|
+
const revision = workItem.rev || workItem._rev || 1;
|
|
969
|
+
// Check field formats
|
|
970
|
+
const formats = checkFieldFormats(workItem);
|
|
971
|
+
if (!formats.ready) {
|
|
972
|
+
const htmlFields = [];
|
|
973
|
+
if (formats.description === 'html')
|
|
974
|
+
htmlFields.push('Description');
|
|
975
|
+
if (formats.acceptanceCriteria === 'html')
|
|
976
|
+
htmlFields.push('Acceptance Criteria');
|
|
977
|
+
skipped.push({
|
|
978
|
+
id: workItemId,
|
|
979
|
+
reason: `HTML fields: ${htmlFields.join(', ')}. Convert to markdown in ADO first.`,
|
|
980
|
+
});
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
// Convert to markdown and save
|
|
984
|
+
const { content: markdown, skippedFields: secondarySkipped } = workItemToMarkdown(workItem, revision);
|
|
985
|
+
const filePath = getWorkItemFilePath(syncConfig.folder, workItemId);
|
|
986
|
+
await writeWorkItemFile(filePath, markdown);
|
|
987
|
+
pulled.push({
|
|
988
|
+
id: workItemId,
|
|
989
|
+
file: filePath,
|
|
990
|
+
revision,
|
|
991
|
+
...(secondarySkipped.length > 0 ? { skippedFields: secondarySkipped } : {}),
|
|
992
|
+
});
|
|
993
|
+
filesToCommit.push({ filePath, workItemId });
|
|
994
|
+
// Optionally save comments
|
|
995
|
+
if (includeComments) {
|
|
996
|
+
const comments = await service.getWorkItemComments(project, workItemId);
|
|
997
|
+
const commentsMarkdown = commentsToMarkdown(workItem, comments.comments || []);
|
|
998
|
+
const commentsPath = getCommentsFilePath(syncConfig.folder, workItemId);
|
|
999
|
+
await writeWorkItemFile(commentsPath, commentsMarkdown);
|
|
1000
|
+
commentsFiles.push({
|
|
1001
|
+
id: workItemId,
|
|
1002
|
+
file: commentsPath,
|
|
1003
|
+
count: (comments.comments || []).length,
|
|
1004
|
+
});
|
|
1005
|
+
filesToCommit.push({ filePath: commentsPath, workItemId });
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
skipped.push({ id: workItemId, reason: error.message });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
// Auto-commit if enabled
|
|
1013
|
+
let committed = false;
|
|
1014
|
+
if (syncConfig.autoCommit && filesToCommit.length > 0) {
|
|
1015
|
+
const commitResult = await autoCommitMultipleFiles(filesToCommit, 'synced');
|
|
1016
|
+
committed = commitResult.committed;
|
|
1017
|
+
}
|
|
1018
|
+
const result = {
|
|
1019
|
+
pulled,
|
|
1020
|
+
skipped,
|
|
1021
|
+
...(includeComments ? { commentsFiles } : {}),
|
|
1022
|
+
folder: syncConfig.folder,
|
|
1023
|
+
committed,
|
|
1024
|
+
};
|
|
1025
|
+
return {
|
|
1026
|
+
content: [{
|
|
1027
|
+
type: "text",
|
|
1028
|
+
text: `Synced ${pulled.length} work item(s) to local files:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1029
|
+
}],
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
console.error("Error syncing work items to files:", error);
|
|
1034
|
+
return {
|
|
1035
|
+
content: [{
|
|
1036
|
+
type: "text",
|
|
1037
|
+
text: `Failed to sync work items: ${error.message}`,
|
|
1038
|
+
}],
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
server.tool("sync-work-item-from-file", "Upload local markdown changes back to ADO. Requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true. Only updates markdown fields.", {
|
|
1043
|
+
project: z.string().describe("The project name"),
|
|
1044
|
+
workItemIds: z.array(z.number()).describe("Work item IDs to push"),
|
|
1045
|
+
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1046
|
+
}, async ({ project, workItemIds, folder }) => {
|
|
1047
|
+
try {
|
|
1048
|
+
const service = getAzureDevOpsService();
|
|
1049
|
+
const syncConfig = getSyncConfig(folder);
|
|
1050
|
+
// Validate folder path for security
|
|
1051
|
+
validateFolderPath(syncConfig.folder);
|
|
1052
|
+
const pushed = [];
|
|
1053
|
+
const partial = [];
|
|
1054
|
+
const failed = [];
|
|
1055
|
+
for (const workItemId of workItemIds) {
|
|
1056
|
+
const filePath = getWorkItemFilePath(syncConfig.folder, workItemId);
|
|
1057
|
+
try {
|
|
1058
|
+
// Check if file exists
|
|
1059
|
+
if (!await fileExists(filePath)) {
|
|
1060
|
+
failed.push({ id: workItemId, error: `File not found: ${filePath}` });
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
// Parse local file
|
|
1064
|
+
const parsed = await readWorkItemFile(filePath);
|
|
1065
|
+
const oldRevision = parsed.frontmatter.lastSyncedRevision;
|
|
1066
|
+
// Fetch current work item from ADO to compare
|
|
1067
|
+
const currentWorkItem = await service.getWorkItem(project, workItemId);
|
|
1068
|
+
// Build patch operations
|
|
1069
|
+
const { operations, skippedFields } = buildPatchOperations(parsed, currentWorkItem);
|
|
1070
|
+
if (operations.length === 0 && skippedFields.length === 0) {
|
|
1071
|
+
// No changes
|
|
1072
|
+
pushed.push({
|
|
1073
|
+
id: workItemId,
|
|
1074
|
+
oldRevision,
|
|
1075
|
+
newRevision: currentWorkItem.rev || currentWorkItem._rev || oldRevision,
|
|
1076
|
+
fieldsUpdated: [],
|
|
1077
|
+
});
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
// Update ADO if there are operations
|
|
1081
|
+
let newRevision = oldRevision;
|
|
1082
|
+
let fieldsUpdated = [];
|
|
1083
|
+
if (operations.length > 0) {
|
|
1084
|
+
const updatedWorkItem = await service.updateWorkItem(project, workItemId, operations);
|
|
1085
|
+
newRevision = updatedWorkItem.rev || updatedWorkItem._rev || oldRevision + 1;
|
|
1086
|
+
fieldsUpdated = operations.map(op => op.path.replace('/fields/', ''));
|
|
1087
|
+
// Update local file with new revision
|
|
1088
|
+
const content = await readFileContent(filePath);
|
|
1089
|
+
const updatedContent = updateSyncRevision(content, newRevision);
|
|
1090
|
+
await writeWorkItemFile(filePath, updatedContent);
|
|
1091
|
+
}
|
|
1092
|
+
if (skippedFields.length > 0) {
|
|
1093
|
+
partial.push({
|
|
1094
|
+
id: workItemId,
|
|
1095
|
+
oldRevision,
|
|
1096
|
+
newRevision,
|
|
1097
|
+
fieldsUpdated,
|
|
1098
|
+
skippedFields,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
pushed.push({
|
|
1103
|
+
id: workItemId,
|
|
1104
|
+
oldRevision,
|
|
1105
|
+
newRevision,
|
|
1106
|
+
fieldsUpdated,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
failed.push({ id: workItemId, error: error.message });
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
const result = {
|
|
1115
|
+
pushed,
|
|
1116
|
+
partial,
|
|
1117
|
+
failed,
|
|
1118
|
+
folder: syncConfig.folder,
|
|
1119
|
+
};
|
|
1120
|
+
return {
|
|
1121
|
+
content: [{
|
|
1122
|
+
type: "text",
|
|
1123
|
+
text: `Pushed ${pushed.length} work item(s) to ADO:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1124
|
+
}],
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
catch (error) {
|
|
1128
|
+
console.error("Error syncing work items from files:", error);
|
|
1129
|
+
return {
|
|
1130
|
+
content: [{
|
|
1131
|
+
type: "text",
|
|
1132
|
+
text: `Failed to push work items: ${error.message}`,
|
|
1133
|
+
}],
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
server.tool("check-work-item-markdown", "Check if work item fields are markdown (required for sync) or HTML format.", {
|
|
1138
|
+
project: z.string().describe("The project name"),
|
|
1139
|
+
workItemIds: z.array(z.number()).describe("Work item IDs to check"),
|
|
1140
|
+
}, async ({ project, workItemIds }) => {
|
|
1141
|
+
try {
|
|
1142
|
+
const service = getAzureDevOpsService();
|
|
1143
|
+
const results = [];
|
|
1144
|
+
let readyCount = 0;
|
|
1145
|
+
let needsConversionCount = 0;
|
|
1146
|
+
for (const workItemId of workItemIds) {
|
|
1147
|
+
try {
|
|
1148
|
+
const workItem = await service.getWorkItem(project, workItemId);
|
|
1149
|
+
const formats = checkFieldFormats(workItem);
|
|
1150
|
+
results.push({
|
|
1151
|
+
id: workItemId,
|
|
1152
|
+
description: formats.description,
|
|
1153
|
+
acceptanceCriteria: formats.acceptanceCriteria,
|
|
1154
|
+
howToTest: formats.additionalFields.howToTest,
|
|
1155
|
+
predeploymentSteps: formats.additionalFields.predeploymentSteps,
|
|
1156
|
+
postdeploymentSteps: formats.additionalFields.postdeploymentSteps,
|
|
1157
|
+
deploymentInformation: formats.additionalFields.deploymentInformation,
|
|
1158
|
+
ready: formats.ready,
|
|
1159
|
+
...(formats.warnings.length > 0 ? { warnings: formats.warnings } : {}),
|
|
1160
|
+
});
|
|
1161
|
+
if (formats.ready) {
|
|
1162
|
+
readyCount++;
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
needsConversionCount++;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
catch (error) {
|
|
1169
|
+
results.push({
|
|
1170
|
+
id: workItemId,
|
|
1171
|
+
description: 'error',
|
|
1172
|
+
acceptanceCriteria: 'error',
|
|
1173
|
+
ready: false,
|
|
1174
|
+
error: error.message || String(error),
|
|
1175
|
+
});
|
|
1176
|
+
needsConversionCount++;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
const result = {
|
|
1180
|
+
results,
|
|
1181
|
+
summary: {
|
|
1182
|
+
ready: readyCount,
|
|
1183
|
+
needsConversion: needsConversionCount,
|
|
1184
|
+
},
|
|
1185
|
+
conversionInstructions: needsConversionCount > 0 ? getConversionInstructions() : undefined,
|
|
1186
|
+
};
|
|
1187
|
+
return {
|
|
1188
|
+
content: [{
|
|
1189
|
+
type: "text",
|
|
1190
|
+
text: `Work item format check:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1191
|
+
}],
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
catch (error) {
|
|
1195
|
+
console.error("Error checking work item formats:", error);
|
|
1196
|
+
return {
|
|
1197
|
+
content: [{
|
|
1198
|
+
type: "text",
|
|
1199
|
+
text: `Failed to check work item formats: ${error.message}`,
|
|
1200
|
+
}],
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
server.tool("list-synced-work-items", "List work items that have been synced to local markdown files.", {
|
|
1205
|
+
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1206
|
+
}, async ({ folder }) => {
|
|
1207
|
+
try {
|
|
1208
|
+
const syncConfig = getSyncConfig(folder);
|
|
1209
|
+
// Validate folder path for security
|
|
1210
|
+
validateFolderPath(syncConfig.folder);
|
|
1211
|
+
const workItems = await listSyncedWorkItems(syncConfig.folder);
|
|
1212
|
+
const result = {
|
|
1213
|
+
workItems,
|
|
1214
|
+
folder: syncConfig.folder,
|
|
1215
|
+
count: workItems.length,
|
|
1216
|
+
};
|
|
1217
|
+
return {
|
|
1218
|
+
content: [{
|
|
1219
|
+
type: "text",
|
|
1220
|
+
text: `Synced work items in ${syncConfig.folder}:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1221
|
+
}],
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
catch (error) {
|
|
1225
|
+
console.error("Error listing synced work items:", error);
|
|
1226
|
+
return {
|
|
1227
|
+
content: [{
|
|
1228
|
+
type: "text",
|
|
1229
|
+
text: `Failed to list synced work items: ${error.message}`,
|
|
1230
|
+
}],
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
// ========================================
|
|
1235
|
+
// TASK SYNC TOOLS
|
|
1236
|
+
// ========================================
|
|
1237
|
+
server.tool("sync-tasks-to-file", "Download all tasks under a parent work item (User Story) to a local markdown file. Supports pulling tasks for multiple parents at once.", {
|
|
1238
|
+
project: z.string().describe("The project name"),
|
|
1239
|
+
parentIds: z.array(z.number()).describe("Parent work item IDs (User Stories) to fetch tasks for"),
|
|
1240
|
+
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1241
|
+
}, async ({ project, parentIds, folder }) => {
|
|
1242
|
+
try {
|
|
1243
|
+
const service = getAzureDevOpsService();
|
|
1244
|
+
const syncConfig = getSyncConfig(folder);
|
|
1245
|
+
// Validate folder path for security
|
|
1246
|
+
validateFolderPath(syncConfig.folder);
|
|
1247
|
+
const pulled = [];
|
|
1248
|
+
const failed = [];
|
|
1249
|
+
const filesToCommit = [];
|
|
1250
|
+
await ensureFolderExists(syncConfig.folder);
|
|
1251
|
+
for (const parentId of parentIds) {
|
|
1252
|
+
try {
|
|
1253
|
+
// Fetch parent work item to get title and validate existence
|
|
1254
|
+
const parentWorkItem = await service.getWorkItem(project, parentId);
|
|
1255
|
+
// Query for child tasks using WIQL
|
|
1256
|
+
const wiql = `SELECT [System.Id] FROM WorkItems WHERE [System.Parent] = ${parentId} AND [System.WorkItemType] = 'Task' ORDER BY [System.Id] ASC`;
|
|
1257
|
+
const queryResult = await service.queryWorkItems(project, wiql, 200);
|
|
1258
|
+
// Fetch full details for each task
|
|
1259
|
+
const tasks = [];
|
|
1260
|
+
if (queryResult.workItems && queryResult.workItems.length > 0) {
|
|
1261
|
+
for (const wi of queryResult.workItems) {
|
|
1262
|
+
try {
|
|
1263
|
+
const task = await service.getWorkItem(project, wi.id);
|
|
1264
|
+
tasks.push(task);
|
|
1265
|
+
}
|
|
1266
|
+
catch (taskError) {
|
|
1267
|
+
console.error(`Error fetching task ${wi.id}:`, taskError.message);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
// Convert to markdown and save
|
|
1272
|
+
const markdown = tasksToMarkdown(parentWorkItem, tasks, project);
|
|
1273
|
+
const filePath = getTasksFilePath(syncConfig.folder, parentId);
|
|
1274
|
+
await writeWorkItemFile(filePath, markdown);
|
|
1275
|
+
pulled.push({
|
|
1276
|
+
parentId,
|
|
1277
|
+
file: filePath,
|
|
1278
|
+
taskCount: tasks.length,
|
|
1279
|
+
});
|
|
1280
|
+
filesToCommit.push({ filePath, workItemId: parentId });
|
|
1281
|
+
}
|
|
1282
|
+
catch (error) {
|
|
1283
|
+
failed.push({ parentId, error: error.message });
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
// Auto-commit if enabled
|
|
1287
|
+
let committed = false;
|
|
1288
|
+
if (syncConfig.autoCommit && filesToCommit.length > 0) {
|
|
1289
|
+
const commitResult = await autoCommitMultipleFiles(filesToCommit, 'synced tasks for');
|
|
1290
|
+
committed = commitResult.committed;
|
|
1291
|
+
}
|
|
1292
|
+
const result = {
|
|
1293
|
+
pulled,
|
|
1294
|
+
failed,
|
|
1295
|
+
folder: syncConfig.folder,
|
|
1296
|
+
committed,
|
|
1297
|
+
};
|
|
1298
|
+
return {
|
|
1299
|
+
content: [{
|
|
1300
|
+
type: "text",
|
|
1301
|
+
text: `Synced tasks for ${pulled.length} parent(s) to local files:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1302
|
+
}],
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
catch (error) {
|
|
1306
|
+
console.error("Error syncing tasks to files:", error);
|
|
1307
|
+
return {
|
|
1308
|
+
content: [{
|
|
1309
|
+
type: "text",
|
|
1310
|
+
text: `Failed to sync tasks: ${error.message}`,
|
|
1311
|
+
}],
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
server.tool("sync-tasks-from-file", "Push local task changes back to ADO with upsert semantics. Existing tasks (## Task #ID) are updated, new tasks (## NEW TASK) are created. Requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true.", {
|
|
1316
|
+
project: z.string().describe("The project name"),
|
|
1317
|
+
parentIds: z.array(z.number()).describe("Parent work item IDs to sync tasks for"),
|
|
1318
|
+
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1319
|
+
}, async ({ project, parentIds, folder }) => {
|
|
1320
|
+
try {
|
|
1321
|
+
const service = getAzureDevOpsService();
|
|
1322
|
+
const syncConfig = getSyncConfig(folder);
|
|
1323
|
+
// Validate folder path for security
|
|
1324
|
+
validateFolderPath(syncConfig.folder);
|
|
1325
|
+
const updated = [];
|
|
1326
|
+
const created = [];
|
|
1327
|
+
const failed = [];
|
|
1328
|
+
for (const parentId of parentIds) {
|
|
1329
|
+
const filePath = getTasksFilePath(syncConfig.folder, parentId);
|
|
1330
|
+
try {
|
|
1331
|
+
// Check if file exists
|
|
1332
|
+
if (!await fileExists(filePath)) {
|
|
1333
|
+
failed.push({ parentId, error: `File not found: ${filePath}` });
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
// Parse local file
|
|
1337
|
+
const content = await readFileContent(filePath);
|
|
1338
|
+
const parsed = parseTasksMarkdown(content);
|
|
1339
|
+
// Get parent work item for area/iteration path
|
|
1340
|
+
const parentWorkItem = await service.getWorkItem(project, parentId);
|
|
1341
|
+
const parentFields = parentWorkItem.fields || {};
|
|
1342
|
+
const areaPath = parentFields['System.AreaPath'];
|
|
1343
|
+
const iterationPath = parentFields['System.IterationPath'];
|
|
1344
|
+
const createdInThisFile = [];
|
|
1345
|
+
// Process each task
|
|
1346
|
+
for (const task of parsed.tasks) {
|
|
1347
|
+
try {
|
|
1348
|
+
if (task.id !== null) {
|
|
1349
|
+
// Existing task - update
|
|
1350
|
+
const currentTask = await service.getWorkItem(project, task.id);
|
|
1351
|
+
const { operations, fieldsUpdated } = buildTaskPatchOperations(task, currentTask);
|
|
1352
|
+
if (operations.length > 0) {
|
|
1353
|
+
await service.updateWorkItem(project, task.id, operations);
|
|
1354
|
+
updated.push({
|
|
1355
|
+
id: task.id,
|
|
1356
|
+
parentId,
|
|
1357
|
+
fieldsUpdated,
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
else {
|
|
1361
|
+
// No changes needed
|
|
1362
|
+
updated.push({
|
|
1363
|
+
id: task.id,
|
|
1364
|
+
parentId,
|
|
1365
|
+
fieldsUpdated: [],
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
// New task - create
|
|
1371
|
+
if (!task.title) {
|
|
1372
|
+
continue; // Skip tasks without title
|
|
1373
|
+
}
|
|
1374
|
+
const fields = buildNewTaskFields(task, areaPath, iterationPath);
|
|
1375
|
+
const createdTask = await service.createWorkItem(project, 'Task', fields, parentId);
|
|
1376
|
+
created.push({
|
|
1377
|
+
id: createdTask.id,
|
|
1378
|
+
parentId,
|
|
1379
|
+
title: task.title,
|
|
1380
|
+
});
|
|
1381
|
+
createdInThisFile.push({
|
|
1382
|
+
title: task.title,
|
|
1383
|
+
id: createdTask.id,
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
catch (taskError) {
|
|
1388
|
+
failed.push({
|
|
1389
|
+
parentId,
|
|
1390
|
+
taskId: task.id || undefined,
|
|
1391
|
+
error: taskError.message,
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
// Update the file with new task IDs if any were created
|
|
1396
|
+
if (createdInThisFile.length > 0) {
|
|
1397
|
+
const updatedContent = updateTasksFileAfterCreate(content, createdInThisFile);
|
|
1398
|
+
await writeWorkItemFile(filePath, updatedContent);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
catch (error) {
|
|
1402
|
+
failed.push({ parentId, error: error.message });
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
const result = {
|
|
1406
|
+
updated,
|
|
1407
|
+
created,
|
|
1408
|
+
failed,
|
|
1409
|
+
folder: syncConfig.folder,
|
|
1410
|
+
};
|
|
1411
|
+
return {
|
|
1412
|
+
content: [{
|
|
1413
|
+
type: "text",
|
|
1414
|
+
text: `Pushed tasks to ADO (${updated.length} updated, ${created.length} created):\n\n${JSON.stringify(result, null, 2)}`,
|
|
1415
|
+
}],
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
catch (error) {
|
|
1419
|
+
console.error("Error syncing tasks from files:", error);
|
|
1420
|
+
return {
|
|
1421
|
+
content: [{
|
|
1422
|
+
type: "text",
|
|
1423
|
+
text: `Failed to push tasks: ${error.message}`,
|
|
1424
|
+
}],
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
903
1428
|
// Log registration summary (enablePullRequestWrite already defined above)
|
|
904
|
-
const
|
|
1429
|
+
const syncToolsCount = 4; // Work item sync tools
|
|
1430
|
+
const taskSyncToolsCount = 2; // Task sync tools
|
|
1431
|
+
const baseToolsCount = 21 + syncToolsCount + taskSyncToolsCount; // 15 original + 6 PR read-only + 4 work item sync + 2 task sync
|
|
905
1432
|
const prWriteToolsCount = enablePullRequestWrite ? 1 : 0;
|
|
906
1433
|
const totalToolsCount = baseToolsCount + prWriteToolsCount;
|
|
907
1434
|
console.error(`azure-devops tools registered: ${totalToolsCount} tools (${baseToolsCount} base + ${prWriteToolsCount} PR write), 4 prompts`);
|