@mcp-consultant-tools/azure-devops 27.0.0-beta.5 → 27.0.0-beta.7
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/AzureDevOpsService.d.ts +33 -0
- package/build/AzureDevOpsService.d.ts.map +1 -1
- package/build/AzureDevOpsService.js +208 -0
- package/build/AzureDevOpsService.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +187 -42
- package/build/index.js.map +1 -1
- package/build/sync/html-converter.d.ts +32 -0
- package/build/sync/html-converter.d.ts.map +1 -0
- package/build/sync/html-converter.js +91 -0
- package/build/sync/html-converter.js.map +1 -0
- package/build/sync/html-detection.d.ts +10 -0
- package/build/sync/html-detection.d.ts.map +1 -1
- package/build/sync/html-detection.js +23 -0
- package/build/sync/html-detection.js.map +1 -1
- package/build/sync/index.d.ts +1 -0
- package/build/sync/index.d.ts.map +1 -1
- package/build/sync/index.js +1 -0
- package/build/sync/index.js.map +1 -1
- package/build/sync/markdown-serializer.d.ts +8 -2
- package/build/sync/markdown-serializer.d.ts.map +1 -1
- package/build/sync/markdown-serializer.js +58 -17
- package/build/sync/markdown-serializer.js.map +1 -1
- package/build/sync/task-serializer.d.ts +2 -1
- package/build/sync/task-serializer.d.ts.map +1 -1
- package/build/sync/task-serializer.js +26 -12
- package/build/sync/task-serializer.js.map +1 -1
- package/build/tool-examples.d.ts +44 -0
- package/build/tool-examples.d.ts.map +1 -0
- package/build/tool-examples.js +122 -0
- package/build/tool-examples.js.map +1 -0
- package/package.json +3 -1
package/build/index.js
CHANGED
|
@@ -10,11 +10,14 @@ 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 {
|
|
13
|
+
import { descWithExamples, WIQL_EXAMPLES, PATCH_OP_EXAMPLES, WORK_ITEM_FIELD_EXAMPLES, SYNC_TO_FILE_EXAMPLES, SYNC_FROM_FILE_EXAMPLES, SYNC_TASKS_TO_FILE_EXAMPLES, SYNC_TASKS_FROM_FILE_EXAMPLES, } from './tool-examples.js';
|
|
14
|
+
import { checkFieldFormats, workItemToMarkdown, commentsToMarkdown, buildPatchOperations, updateSyncRevision, getSyncConfig, ensureFolderExists, getWorkItemFilePath, getCommentsFilePath, fileExists, writeWorkItemFile, readWorkItemFile, readFileContent, listSyncedWorkItems, validateFolderPath, autoCommitMultipleFiles,
|
|
14
15
|
// Task sync utilities
|
|
15
16
|
tasksToMarkdown, parseTasksMarkdown, buildTaskPatchOperations, buildNewTaskFields, getTasksFilePath, updateTasksFileAfterCreate,
|
|
16
17
|
// New work item utilities
|
|
17
|
-
parseNewWorkItemMarkdown, buildNewWorkItemFields, generateNewWorkItemTemplate, convertNewFileToSynced, getNewWorkItemFilePath, findNextNewFileIndex, findNewWorkItemFiles, renameFile,
|
|
18
|
+
parseNewWorkItemMarkdown, buildNewWorkItemFields, generateNewWorkItemTemplate, convertNewFileToSynced, getNewWorkItemFilePath, findNextNewFileIndex, findNewWorkItemFiles, renameFile,
|
|
19
|
+
// Markdown format utilities
|
|
20
|
+
getAllLargeTextFields, autoConvertFieldsToMarkdown, isHtmlContent, } from './sync/index.js';
|
|
18
21
|
/**
|
|
19
22
|
* Register azure-devops tools and prompts to an MCP server
|
|
20
23
|
* @param server - The MCP server instance
|
|
@@ -579,7 +582,7 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
579
582
|
});
|
|
580
583
|
server.tool("query-work-items", "Query work items using WIQL (Work Item Query Language) in Azure DevOps", {
|
|
581
584
|
project: z.string().describe("The project name"),
|
|
582
|
-
wiql: z.string().describe("The WIQL query string
|
|
585
|
+
wiql: z.string().describe(descWithExamples("The WIQL query string. SQL-like syntax with field names in brackets. Common fields: [System.Id], [System.Title], [System.State], [System.WorkItemType], [System.AssignedTo], [System.Parent]. Use @Me for current user.", WIQL_EXAMPLES)),
|
|
583
586
|
maxResults: z.number().optional().describe("Maximum number of results (default: 200)"),
|
|
584
587
|
}, async ({ project, wiql, maxResults }) => {
|
|
585
588
|
try {
|
|
@@ -666,24 +669,53 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
666
669
|
};
|
|
667
670
|
}
|
|
668
671
|
});
|
|
669
|
-
server.tool("update-work-item", "Update a work item in Azure DevOps using JSON Patch operations (requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true)", {
|
|
672
|
+
server.tool("update-work-item", "Update a work item in Azure DevOps using JSON Patch operations (requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true). Auto-injects markdown format operations for large text fields.", {
|
|
670
673
|
project: z.string().describe("The project name"),
|
|
671
674
|
workItemId: z.number().describe("The work item ID"),
|
|
672
675
|
patchOperations: z.array(z.object({
|
|
673
|
-
op: z.string().describe("The operation type
|
|
674
|
-
path: z.string().describe("The field path (e.g., '/fields/System.State')"),
|
|
676
|
+
op: z.string().describe("The operation type: 'add' (set value), 'replace' (update existing), or 'remove' (clear field)"),
|
|
677
|
+
path: z.string().describe("The field path starting with '/fields/' (e.g., '/fields/System.State', '/fields/System.Title')"),
|
|
675
678
|
value: z.any().optional().describe("The value to set (not required for 'remove' operation)")
|
|
676
|
-
})).describe("Array of JSON Patch operations"),
|
|
677
|
-
|
|
679
|
+
})).describe(descWithExamples("Array of JSON Patch operations. Each operation specifies what to change.", PATCH_OP_EXAMPLES)),
|
|
680
|
+
skipAutoConvert: z.boolean().optional().describe("Skip automatic markdown format injection for large text fields. Only use when explicitly requested. Default: false"),
|
|
681
|
+
}, async ({ project, workItemId, patchOperations, skipAutoConvert }) => {
|
|
678
682
|
try {
|
|
679
683
|
const service = getAzureDevOpsService();
|
|
680
|
-
|
|
684
|
+
// Auto-inject markdown format operations for large text fields
|
|
685
|
+
let finalOperations = [...patchOperations];
|
|
686
|
+
const formatOpsAdded = [];
|
|
687
|
+
if (!skipAutoConvert) {
|
|
688
|
+
// Find large text fields being updated (includes custom fields)
|
|
689
|
+
const allLargeTextFields = getAllLargeTextFields();
|
|
690
|
+
const largeTextFieldsBeingUpdated = patchOperations
|
|
691
|
+
.filter((op) => op.path?.startsWith('/fields/'))
|
|
692
|
+
.map((op) => op.path.replace('/fields/', ''))
|
|
693
|
+
.filter((field) => allLargeTextFields.includes(field));
|
|
694
|
+
for (const field of largeTextFieldsBeingUpdated) {
|
|
695
|
+
// Check if format operation already exists
|
|
696
|
+
const hasFormatOp = finalOperations.some((op) => op.path === `/multilineFieldsFormat/${field}`);
|
|
697
|
+
if (!hasFormatOp) {
|
|
698
|
+
// Add format operation to ensure markdown
|
|
699
|
+
finalOperations.push({
|
|
700
|
+
op: 'add',
|
|
701
|
+
path: `/multilineFieldsFormat/${field}`,
|
|
702
|
+
value: 'Markdown'
|
|
703
|
+
});
|
|
704
|
+
formatOpsAdded.push(field);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const result = await service.updateWorkItem(project, workItemId, finalOperations);
|
|
709
|
+
let message = `Updated work item ${workItemId}`;
|
|
710
|
+
if (formatOpsAdded.length > 0) {
|
|
711
|
+
message += ` (auto-set markdown format for: ${formatOpsAdded.join(', ')})`;
|
|
712
|
+
}
|
|
681
713
|
const resultStr = JSON.stringify(result, null, 2);
|
|
682
714
|
return {
|
|
683
715
|
content: [
|
|
684
716
|
{
|
|
685
717
|
type: "text",
|
|
686
|
-
text:
|
|
718
|
+
text: `${message}:\n\n${resultStr}`,
|
|
687
719
|
},
|
|
688
720
|
],
|
|
689
721
|
};
|
|
@@ -702,8 +734,8 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
702
734
|
});
|
|
703
735
|
server.tool("create-work-item", "Create a new work item in Azure DevOps with optional parent relationship (requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true)", {
|
|
704
736
|
project: z.string().describe("The project name"),
|
|
705
|
-
workItemType: z.string().describe("The work item type
|
|
706
|
-
fields: z.record(z.any()).describe("Object with field values
|
|
737
|
+
workItemType: z.string().describe("The work item type: 'Bug', 'Task', 'User Story', 'Feature', 'Epic', or custom types"),
|
|
738
|
+
fields: z.record(z.any()).describe(descWithExamples("Object with field values. Required: System.Title. Common fields: System.Description, System.State, System.AssignedTo, Microsoft.VSTS.Common.AcceptanceCriteria, Microsoft.VSTS.TCM.ReproSteps (bugs).", WORK_ITEM_FIELD_EXAMPLES)),
|
|
707
739
|
parentId: z.number().optional().describe("Optional parent work item ID (for creating child items). Simplified alternative to relations parameter."),
|
|
708
740
|
relations: z.array(z.object({
|
|
709
741
|
rel: z.string().describe("Relation type (e.g., 'System.LinkTypes.Hierarchy-Reverse' for parent)"),
|
|
@@ -945,16 +977,70 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
945
977
|
}
|
|
946
978
|
});
|
|
947
979
|
// ========================================
|
|
980
|
+
// BUILD TROUBLESHOOTING TOOLS (Read-only)
|
|
981
|
+
// NOTE: These tools are duplicated in azure-devops-admin package.
|
|
982
|
+
// If you update these, also update packages/azure-devops-admin/src/index.ts
|
|
983
|
+
// ========================================
|
|
984
|
+
server.tool("get-build-status", "Get build status and details. Use detail='summary' for basic status, 'timeline' for step breakdown (default scope='problems' shows only failed/warning items), or 'full' for logs. The timelineScope controls what records are included: 'problems' (default, only errors/warnings), 'stages' (minimal), 'jobs' (moderate), 'all' (everything).", {
|
|
985
|
+
project: z.string().describe("The project name"),
|
|
986
|
+
buildId: z.number().describe("The build ID"),
|
|
987
|
+
detail: z.enum(["summary", "timeline", "full"]).optional().describe("Level of detail: 'summary' (default), 'timeline' (include steps), or 'full' (include logs)"),
|
|
988
|
+
timelineScope: z.enum(["stages", "jobs", "all", "problems"]).optional().describe("Timeline scope: 'problems' (default, only errors/warnings/failures), 'stages' (minimal), 'jobs' (moderate), 'all' (everything - may be large)"),
|
|
989
|
+
maxIssues: z.number().optional().describe("Maximum issues per record (default: 5, prioritizes errors over warnings)"),
|
|
990
|
+
}, async ({ project, buildId, detail, timelineScope, maxIssues }) => {
|
|
991
|
+
try {
|
|
992
|
+
const service = getAzureDevOpsService();
|
|
993
|
+
const result = await service.getBuildStatus(project, buildId, detail || 'summary', timelineScope || 'problems', maxIssues || 5);
|
|
994
|
+
return { content: [{ type: "text", text: `Build ${buildId} status:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
995
|
+
}
|
|
996
|
+
catch (error) {
|
|
997
|
+
console.error("Error getting build status:", error);
|
|
998
|
+
return { content: [{ type: "text", text: `Failed to get build status: ${error.message}` }] };
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
server.tool("get-build-timeline", "Get step-by-step breakdown of a build. Shows stages, jobs, and tasks with timing, status, and error/warning counts. Use scope to control output size: 'problems' (default, only errors/warnings), 'stages' (minimal), 'jobs' (moderate), 'all' (everything). Always includes summary stats regardless of scope.", {
|
|
1002
|
+
project: z.string().describe("The project name"),
|
|
1003
|
+
buildId: z.number().describe("The build ID"),
|
|
1004
|
+
scope: z.enum(["stages", "jobs", "all", "problems"]).optional().describe("Filter scope: 'problems' (default, only errors/warnings/failures), 'stages' (minimal), 'jobs' (moderate), 'all' (everything - may be large)"),
|
|
1005
|
+
maxIssues: z.number().optional().describe("Maximum issues per record (default: 5, prioritizes errors over warnings)"),
|
|
1006
|
+
}, async ({ project, buildId, scope, maxIssues }) => {
|
|
1007
|
+
try {
|
|
1008
|
+
const service = getAzureDevOpsService();
|
|
1009
|
+
const result = await service.getBuildTimeline(project, buildId, scope || 'problems', maxIssues || 5);
|
|
1010
|
+
return { content: [{ type: "text", text: `Build ${buildId} timeline:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
console.error("Error getting build timeline:", error);
|
|
1014
|
+
return { content: [{ type: "text", text: `Failed to get build timeline: ${error.message}` }] };
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
server.tool("get-build-logs", "Get build logs. Without logId, returns list of available logs with line counts. With logId, returns that log's content. NOTE: For verbose logs, consider using a sub-agent to analyze the content.", {
|
|
1018
|
+
project: z.string().describe("The project name"),
|
|
1019
|
+
buildId: z.number().describe("The build ID"),
|
|
1020
|
+
logId: z.number().optional().describe("Optional specific log ID to retrieve content"),
|
|
1021
|
+
}, async ({ project, buildId, logId }) => {
|
|
1022
|
+
try {
|
|
1023
|
+
const service = getAzureDevOpsService();
|
|
1024
|
+
const result = await service.getBuildLogs(project, buildId, logId);
|
|
1025
|
+
return { content: [{ type: "text", text: `Build ${buildId} logs:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
console.error("Error getting build logs:", error);
|
|
1029
|
+
return { content: [{ type: "text", text: `Failed to get build logs: ${error.message}` }] };
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
// ========================================
|
|
948
1033
|
// WORK ITEM SYNC TOOLS (Local Markdown)
|
|
949
1034
|
// ========================================
|
|
950
|
-
server.tool("sync-work-item-to-file", "Download work item(s) from ADO and save as local markdown file(s). Token-efficient for editing.
|
|
1035
|
+
server.tool("sync-work-item-to-file", descWithExamples("Download work item(s) from ADO and save as local markdown file(s). Token-efficient for editing. Auto-converts HTML fields to markdown. Can also pull all child work items under a parent (e.g., all User Stories under a Feature).", SYNC_TO_FILE_EXAMPLES), {
|
|
951
1036
|
project: z.string().describe("The project name"),
|
|
952
1037
|
workItemIds: z.array(z.number()).default([]).describe("Work item IDs to pull (optional if using parentId)"),
|
|
953
1038
|
parentId: z.number().optional().describe("Pull all child work items of this parent (e.g., Feature ID to pull all User Stories)"),
|
|
954
|
-
childType: z.string().optional().describe("Filter by work item type when using parentId (default: 'User Story')"),
|
|
1039
|
+
childType: z.string().optional().describe("Filter by work item type when using parentId (default: 'User Story'). Common values: 'User Story', 'Bug', 'Task'"),
|
|
955
1040
|
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
956
1041
|
includeComments: z.boolean().optional().describe("Also save comments to {id}-comments.md (default: false)"),
|
|
957
|
-
|
|
1042
|
+
skipAutoConvert: z.boolean().optional().describe("Skip automatic HTML-to-markdown conversion. Only use when explicitly requested. Default: false (auto-convert enabled)"),
|
|
1043
|
+
}, async ({ project, workItemIds: providedWorkItemIds, parentId, childType, folder, includeComments, skipAutoConvert }) => {
|
|
958
1044
|
try {
|
|
959
1045
|
const service = getAzureDevOpsService();
|
|
960
1046
|
const syncConfig = getSyncConfig(folder);
|
|
@@ -993,21 +1079,44 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
993
1079
|
for (const workItemId of workItemIds) {
|
|
994
1080
|
try {
|
|
995
1081
|
// Fetch work item from ADO
|
|
996
|
-
|
|
997
|
-
|
|
1082
|
+
let workItem = await service.getWorkItem(project, workItemId);
|
|
1083
|
+
let revision = workItem.rev || workItem._rev || 1;
|
|
1084
|
+
let convertedFields = [];
|
|
998
1085
|
// Check field formats
|
|
999
1086
|
const formats = checkFieldFormats(workItem);
|
|
1000
1087
|
if (!formats.ready) {
|
|
1001
|
-
|
|
1088
|
+
if (skipAutoConvert) {
|
|
1089
|
+
// User explicitly requested to skip conversion
|
|
1090
|
+
const htmlFields = [];
|
|
1091
|
+
if (formats.description === 'html')
|
|
1092
|
+
htmlFields.push('Description');
|
|
1093
|
+
if (formats.acceptanceCriteria === 'html')
|
|
1094
|
+
htmlFields.push('Acceptance Criteria');
|
|
1095
|
+
skipped.push({
|
|
1096
|
+
id: workItemId,
|
|
1097
|
+
reason: `HTML fields: ${htmlFields.join(', ')}. skipAutoConvert=true, skipping.`,
|
|
1098
|
+
});
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
// Auto-convert HTML fields to markdown in ADO
|
|
1102
|
+
const fieldsToConvert = [];
|
|
1002
1103
|
if (formats.description === 'html')
|
|
1003
|
-
|
|
1104
|
+
fieldsToConvert.push('System.Description');
|
|
1004
1105
|
if (formats.acceptanceCriteria === 'html')
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1106
|
+
fieldsToConvert.push('Microsoft.VSTS.Common.AcceptanceCriteria');
|
|
1107
|
+
try {
|
|
1108
|
+
convertedFields = await autoConvertFieldsToMarkdown(service, project, workItemId, workItem.fields, fieldsToConvert);
|
|
1109
|
+
// Re-fetch work item with converted content
|
|
1110
|
+
workItem = await service.getWorkItem(project, workItemId);
|
|
1111
|
+
revision = workItem.rev || workItem._rev || 1;
|
|
1112
|
+
}
|
|
1113
|
+
catch (convertError) {
|
|
1114
|
+
skipped.push({
|
|
1115
|
+
id: workItemId,
|
|
1116
|
+
reason: `Failed to auto-convert HTML: ${convertError.message}`,
|
|
1117
|
+
});
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1011
1120
|
}
|
|
1012
1121
|
// Convert to markdown and save
|
|
1013
1122
|
const { content: markdown, skippedFields: secondarySkipped } = workItemToMarkdown(workItem, revision);
|
|
@@ -1017,6 +1126,7 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
1017
1126
|
id: workItemId,
|
|
1018
1127
|
file: filePath,
|
|
1019
1128
|
revision,
|
|
1129
|
+
...(convertedFields.length > 0 ? { converted: convertedFields } : {}),
|
|
1020
1130
|
...(secondarySkipped.length > 0 ? { skippedFields: secondarySkipped } : {}),
|
|
1021
1131
|
});
|
|
1022
1132
|
filesToCommit.push({ filePath, workItemId });
|
|
@@ -1068,11 +1178,12 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
1068
1178
|
};
|
|
1069
1179
|
}
|
|
1070
1180
|
});
|
|
1071
|
-
server.tool("sync-work-item-from-file", "Upload local markdown changes back to ADO. Auto-detects new_*.md files and creates them as new work items. Requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true.", {
|
|
1181
|
+
server.tool("sync-work-item-from-file", descWithExamples("Upload local markdown changes back to ADO. Auto-detects new_*.md files and creates them as new work items. Auto-converts HTML fields to markdown. Requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true.", SYNC_FROM_FILE_EXAMPLES), {
|
|
1072
1182
|
project: z.string().describe("The project name"),
|
|
1073
1183
|
workItemIds: z.array(z.number()).default([]).describe("Work item IDs to push (optional - new_*.md files are auto-detected)"),
|
|
1074
1184
|
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1075
|
-
|
|
1185
|
+
skipAutoConvert: z.boolean().optional().describe("Skip automatic HTML-to-markdown conversion. Only use when explicitly requested. Default: false (auto-convert enabled)"),
|
|
1186
|
+
}, async ({ project, workItemIds, folder, skipAutoConvert }) => {
|
|
1076
1187
|
try {
|
|
1077
1188
|
const service = getAzureDevOpsService();
|
|
1078
1189
|
const syncConfig = getSyncConfig(folder);
|
|
@@ -1138,8 +1249,8 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
1138
1249
|
const oldRevision = parsed.frontmatter.lastSyncedRevision;
|
|
1139
1250
|
// Fetch current work item from ADO to compare
|
|
1140
1251
|
const currentWorkItem = await service.getWorkItem(project, workItemId);
|
|
1141
|
-
// Build patch operations
|
|
1142
|
-
const { operations, skippedFields } = buildPatchOperations(parsed, currentWorkItem);
|
|
1252
|
+
// Build patch operations (auto-converts HTML fields unless skipAutoConvert)
|
|
1253
|
+
const { operations, skippedFields, convertedFields } = buildPatchOperations(parsed, currentWorkItem, skipAutoConvert);
|
|
1143
1254
|
if (operations.length === 0 && skippedFields.length === 0) {
|
|
1144
1255
|
// No changes
|
|
1145
1256
|
pushed.push({
|
|
@@ -1156,7 +1267,10 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
1156
1267
|
if (operations.length > 0) {
|
|
1157
1268
|
const updatedWorkItem = await service.updateWorkItem(project, workItemId, operations);
|
|
1158
1269
|
newRevision = updatedWorkItem.rev || updatedWorkItem._rev || oldRevision + 1;
|
|
1159
|
-
|
|
1270
|
+
// Filter out format operations from fieldsUpdated
|
|
1271
|
+
fieldsUpdated = operations
|
|
1272
|
+
.filter(op => op.path.startsWith('/fields/'))
|
|
1273
|
+
.map(op => op.path.replace('/fields/', ''));
|
|
1160
1274
|
// Update local file with new revision
|
|
1161
1275
|
const content = await readFileContent(filePath);
|
|
1162
1276
|
const updatedContent = updateSyncRevision(content, newRevision);
|
|
@@ -1169,6 +1283,7 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
1169
1283
|
newRevision,
|
|
1170
1284
|
fieldsUpdated,
|
|
1171
1285
|
skippedFields,
|
|
1286
|
+
...(convertedFields.length > 0 ? { convertedFields } : {}),
|
|
1172
1287
|
});
|
|
1173
1288
|
}
|
|
1174
1289
|
else {
|
|
@@ -1177,6 +1292,7 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
1177
1292
|
oldRevision,
|
|
1178
1293
|
newRevision,
|
|
1179
1294
|
fieldsUpdated,
|
|
1295
|
+
...(convertedFields.length > 0 ? { convertedFields } : {}),
|
|
1180
1296
|
});
|
|
1181
1297
|
}
|
|
1182
1298
|
}
|
|
@@ -1265,7 +1381,10 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
1265
1381
|
ready: readyCount,
|
|
1266
1382
|
needsConversion: needsConversionCount,
|
|
1267
1383
|
},
|
|
1268
|
-
|
|
1384
|
+
autoConvertAvailable: true,
|
|
1385
|
+
message: needsConversionCount === 0
|
|
1386
|
+
? 'All work items are markdown format - ready to sync'
|
|
1387
|
+
: `${needsConversionCount} work item(s) have HTML fields. Will be auto-converted to markdown on sync (unless skipAutoConvert=true).`,
|
|
1269
1388
|
};
|
|
1270
1389
|
return {
|
|
1271
1390
|
content: [{
|
|
@@ -1365,11 +1484,12 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
1365
1484
|
// ========================================
|
|
1366
1485
|
// TASK SYNC TOOLS
|
|
1367
1486
|
// ========================================
|
|
1368
|
-
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.", {
|
|
1487
|
+
server.tool("sync-tasks-to-file", descWithExamples("Download all tasks under a parent work item (User Story) to a local markdown file. Auto-converts HTML descriptions to markdown. Supports pulling tasks for multiple parents at once.", SYNC_TASKS_TO_FILE_EXAMPLES), {
|
|
1369
1488
|
project: z.string().describe("The project name"),
|
|
1370
1489
|
parentIds: z.array(z.number()).describe("Parent work item IDs (User Stories) to fetch tasks for"),
|
|
1371
1490
|
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1372
|
-
|
|
1491
|
+
skipAutoConvert: z.boolean().optional().describe("Skip automatic HTML-to-markdown conversion for task descriptions. Only use when explicitly requested. Default: false"),
|
|
1492
|
+
}, async ({ project, parentIds, folder, skipAutoConvert }) => {
|
|
1373
1493
|
try {
|
|
1374
1494
|
const service = getAzureDevOpsService();
|
|
1375
1495
|
const syncConfig = getSyncConfig(folder);
|
|
@@ -1386,12 +1506,26 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
1386
1506
|
// Query for child tasks using WIQL
|
|
1387
1507
|
const wiql = `SELECT [System.Id] FROM WorkItems WHERE [System.Parent] = ${parentId} AND [System.WorkItemType] = 'Task' ORDER BY [System.Id] ASC`;
|
|
1388
1508
|
const queryResult = await service.queryWorkItems(project, wiql, 200);
|
|
1389
|
-
// Fetch full details for each task
|
|
1509
|
+
// Fetch full details for each task and auto-convert HTML descriptions
|
|
1390
1510
|
const tasks = [];
|
|
1511
|
+
let tasksConverted = 0;
|
|
1391
1512
|
if (queryResult.workItems && queryResult.workItems.length > 0) {
|
|
1392
1513
|
for (const wi of queryResult.workItems) {
|
|
1393
1514
|
try {
|
|
1394
|
-
|
|
1515
|
+
let task = await service.getWorkItem(project, wi.id);
|
|
1516
|
+
// Auto-convert HTML description to markdown unless skipAutoConvert
|
|
1517
|
+
const description = task.fields?.['System.Description'];
|
|
1518
|
+
if (description && !skipAutoConvert && isHtmlContent(description)) {
|
|
1519
|
+
try {
|
|
1520
|
+
await autoConvertFieldsToMarkdown(service, project, wi.id, task.fields, ['System.Description']);
|
|
1521
|
+
// Re-fetch with converted content
|
|
1522
|
+
task = await service.getWorkItem(project, wi.id);
|
|
1523
|
+
tasksConverted++;
|
|
1524
|
+
}
|
|
1525
|
+
catch (convertError) {
|
|
1526
|
+
console.error(`Failed to convert HTML description for task ${wi.id}:`, convertError.message);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1395
1529
|
tasks.push(task);
|
|
1396
1530
|
}
|
|
1397
1531
|
catch (taskError) {
|
|
@@ -1443,11 +1577,12 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
1443
1577
|
};
|
|
1444
1578
|
}
|
|
1445
1579
|
});
|
|
1446
|
-
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.", {
|
|
1580
|
+
server.tool("sync-tasks-from-file", descWithExamples("Push local task changes back to ADO with upsert semantics. Existing tasks (## Task #ID) are updated, new tasks (## NEW TASK) are created. Auto-converts HTML fields to markdown. Requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true.", SYNC_TASKS_FROM_FILE_EXAMPLES), {
|
|
1447
1581
|
project: z.string().describe("The project name"),
|
|
1448
1582
|
parentIds: z.array(z.number()).describe("Parent work item IDs to sync tasks for"),
|
|
1449
1583
|
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1450
|
-
|
|
1584
|
+
skipAutoConvert: z.boolean().optional().describe("Skip automatic HTML-to-markdown conversion for task descriptions. Only use when explicitly requested. Default: false"),
|
|
1585
|
+
}, async ({ project, parentIds, folder, skipAutoConvert }) => {
|
|
1451
1586
|
try {
|
|
1452
1587
|
const service = getAzureDevOpsService();
|
|
1453
1588
|
const syncConfig = getSyncConfig(folder);
|
|
@@ -1477,15 +1612,16 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
1477
1612
|
for (const task of parsed.tasks) {
|
|
1478
1613
|
try {
|
|
1479
1614
|
if (task.id !== null) {
|
|
1480
|
-
// Existing task - update
|
|
1615
|
+
// Existing task - update (auto-converts HTML fields unless skipAutoConvert)
|
|
1481
1616
|
const currentTask = await service.getWorkItem(project, task.id);
|
|
1482
|
-
const { operations, fieldsUpdated } = buildTaskPatchOperations(task, currentTask);
|
|
1617
|
+
const { operations, fieldsUpdated, convertedFields } = buildTaskPatchOperations(task, currentTask, skipAutoConvert);
|
|
1483
1618
|
if (operations.length > 0) {
|
|
1484
1619
|
await service.updateWorkItem(project, task.id, operations);
|
|
1485
1620
|
updated.push({
|
|
1486
1621
|
id: task.id,
|
|
1487
1622
|
parentId,
|
|
1488
1623
|
fieldsUpdated,
|
|
1624
|
+
...(convertedFields.length > 0 ? { convertedFields } : {}),
|
|
1489
1625
|
});
|
|
1490
1626
|
}
|
|
1491
1627
|
else {
|
|
@@ -1557,9 +1693,18 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
1557
1693
|
}
|
|
1558
1694
|
});
|
|
1559
1695
|
// Log registration summary (enablePullRequestWrite already defined above)
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1696
|
+
// Tool count breakdown:
|
|
1697
|
+
// - Wiki: 6 (get-wikis, search-wiki-pages, get-wiki-page, create-wiki-page, update-wiki-page, str-replace-wiki-page)
|
|
1698
|
+
// - Work Item: 7 (get-work-item, query-work-items, get-work-item-comments, add-work-item-comment, update-work-item, create-work-item, delete-work-item)
|
|
1699
|
+
// - Variable Group: 2 (list-variable-groups, get-variable-group)
|
|
1700
|
+
// - PR Read-only: 6 (list-repositories, list-pull-requests, get-pull-request, get-pull-request-threads, get-pull-request-commits, get-pull-request-changes)
|
|
1701
|
+
// - Build Troubleshooting: 3 (get-build-status, get-build-timeline, get-build-logs)
|
|
1702
|
+
// - Work Item Sync: 5 (sync-work-item-to-file, sync-work-item-from-file, check-work-item-markdown, list-synced-work-items, create-user-story-file)
|
|
1703
|
+
// - Task Sync: 2 (sync-tasks-to-file, sync-tasks-from-file)
|
|
1704
|
+
// - Config: 1 (get-configuration)
|
|
1705
|
+
// Total base: 6 + 7 + 2 + 6 + 3 + 5 + 2 + 1 = 32
|
|
1706
|
+
// + PR Write: 1 (add-pull-request-thread) - conditional
|
|
1707
|
+
const baseToolsCount = 32;
|
|
1563
1708
|
const prWriteToolsCount = enablePullRequestWrite ? 1 : 0;
|
|
1564
1709
|
const totalToolsCount = baseToolsCount + prWriteToolsCount;
|
|
1565
1710
|
console.error(`azure-devops tools registered: ${totalToolsCount} tools (${baseToolsCount} base + ${prWriteToolsCount} PR write), 4 prompts`);
|