@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.
@@ -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;AAI7D;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,GAAG,EAAE,kBAAkB,CAAC,EAAE,kBAAkB,QAkkC5F;AAED;;GAEG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,iBAAiB,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 baseToolsCount = 21; // 15 original + 6 PR read-only tools
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`);