@mcp-consultant-tools/azure-devops 27.0.0 → 28.0.0-beta.1
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/azure-devops-client.d.ts +42 -0
- package/build/azure-devops-client.d.ts.map +1 -0
- package/build/azure-devops-client.js +144 -0
- package/build/azure-devops-client.js.map +1 -0
- package/build/index.d.ts +8 -11
- package/build/index.d.ts.map +1 -1
- package/build/index.js +40 -1878
- package/build/index.js.map +1 -1
- package/build/models/api-types.d.ts +159 -0
- package/build/models/api-types.d.ts.map +1 -0
- package/build/models/api-types.js +5 -0
- package/build/models/api-types.js.map +1 -0
- package/build/models/index.d.ts +5 -0
- package/build/models/index.d.ts.map +1 -0
- package/build/models/index.js +5 -0
- package/build/models/index.js.map +1 -0
- package/build/prompts/index.d.ts +3 -0
- package/build/prompts/index.d.ts.map +1 -0
- package/build/prompts/index.js +83 -0
- package/build/prompts/index.js.map +1 -0
- package/build/prompts/templates.d.ts +8 -0
- package/build/prompts/templates.d.ts.map +1 -0
- package/build/prompts/templates.js +126 -0
- package/build/prompts/templates.js.map +1 -0
- package/build/services/build-service.d.ts +16 -0
- package/build/services/build-service.d.ts.map +1 -0
- package/build/services/build-service.js +195 -0
- package/build/services/build-service.js.map +1 -0
- package/build/services/configuration-service.d.ts +7 -0
- package/build/services/configuration-service.d.ts.map +1 -0
- package/build/services/configuration-service.js +24 -0
- package/build/services/configuration-service.js.map +1 -0
- package/build/services/index.d.ts +11 -0
- package/build/services/index.d.ts.map +1 -0
- package/build/services/index.js +11 -0
- package/build/services/index.js.map +1 -0
- package/build/services/pull-request-service.d.ts +29 -0
- package/build/services/pull-request-service.d.ts.map +1 -0
- package/build/services/pull-request-service.js +390 -0
- package/build/services/pull-request-service.js.map +1 -0
- package/build/services/sync-service.d.ts +19 -0
- package/build/services/sync-service.d.ts.map +1 -0
- package/build/services/sync-service.js +439 -0
- package/build/services/sync-service.js.map +1 -0
- package/build/services/variable-group-service.d.ts +11 -0
- package/build/services/variable-group-service.d.ts.map +1 -0
- package/build/services/variable-group-service.js +62 -0
- package/build/services/variable-group-service.js.map +1 -0
- package/build/services/wiki-service.d.ts +41 -0
- package/build/services/wiki-service.d.ts.map +1 -0
- package/build/services/wiki-service.js +346 -0
- package/build/services/wiki-service.js.map +1 -0
- package/build/services/work-item-service.d.ts +22 -0
- package/build/services/work-item-service.d.ts.map +1 -0
- package/build/services/work-item-service.js +284 -0
- package/build/services/work-item-service.js.map +1 -0
- package/build/sync/file-utils.d.ts +7 -5
- package/build/sync/file-utils.d.ts.map +1 -1
- package/build/sync/file-utils.js +17 -8
- package/build/sync/file-utils.js.map +1 -1
- package/build/sync/markdown-serializer.d.ts +5 -4
- package/build/sync/markdown-serializer.d.ts.map +1 -1
- package/build/sync/markdown-serializer.js +21 -9
- package/build/sync/markdown-serializer.js.map +1 -1
- package/build/tool-examples.d.ts +8 -0
- package/build/tool-examples.d.ts.map +1 -1
- package/build/tool-examples.js +28 -0
- package/build/tool-examples.js.map +1 -1
- package/build/tools/build-tools.d.ts +3 -0
- package/build/tools/build-tools.d.ts.map +1 -0
- package/build/tools/build-tools.js +56 -0
- package/build/tools/build-tools.js.map +1 -0
- package/build/tools/configuration-tools.d.ts +6 -0
- package/build/tools/configuration-tools.d.ts.map +1 -0
- package/build/tools/configuration-tools.js +30 -0
- package/build/tools/configuration-tools.js.map +1 -0
- package/build/tools/index.d.ts +13 -0
- package/build/tools/index.d.ts.map +1 -0
- package/build/tools/index.js +30 -0
- package/build/tools/index.js.map +1 -0
- package/build/tools/pull-request-tools.d.ts +5 -0
- package/build/tools/pull-request-tools.d.ts.map +1 -0
- package/build/tools/pull-request-tools.js +227 -0
- package/build/tools/pull-request-tools.js.map +1 -0
- package/build/tools/sync-tools.d.ts +3 -0
- package/build/tools/sync-tools.d.ts.map +1 -0
- package/build/tools/sync-tools.js +146 -0
- package/build/tools/sync-tools.js.map +1 -0
- package/build/tools/variable-group-tools.d.ts +3 -0
- package/build/tools/variable-group-tools.d.ts.map +1 -0
- package/build/tools/variable-group-tools.js +32 -0
- package/build/tools/variable-group-tools.js.map +1 -0
- package/build/tools/wiki-tools.d.ts +3 -0
- package/build/tools/wiki-tools.d.ts.map +1 -0
- package/build/tools/wiki-tools.js +151 -0
- package/build/tools/wiki-tools.js.map +1 -0
- package/build/tools/work-item-tools.d.ts +3 -0
- package/build/tools/work-item-tools.d.ts.map +1 -0
- package/build/tools/work-item-tools.js +188 -0
- package/build/tools/work-item-tools.js.map +1 -0
- package/build/types.d.ts +23 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +2 -2
- package/build/AzureDevOpsService.d.ts +0 -489
- package/build/AzureDevOpsService.d.ts.map +0 -1
- package/build/AzureDevOpsService.js +0 -1558
- package/build/AzureDevOpsService.js.map +0 -1
package/build/index.js
CHANGED
|
@@ -2,31 +2,24 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @mcp-consultant-tools/azure-devops
|
|
4
4
|
*
|
|
5
|
-
* MCP server for
|
|
5
|
+
* MCP server for Azure DevOps integration.
|
|
6
|
+
* Entry point: MCP server startup + backward-compatible registerAzureDevOpsTools().
|
|
6
7
|
*/
|
|
7
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
9
|
import { pathToFileURL } from "node:url";
|
|
9
10
|
import { realpathSync } from "node:fs";
|
|
10
11
|
import { createMcpServer, createEnvLoader } from "@mcp-consultant-tools/core";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
// Task sync utilities
|
|
16
|
-
tasksToMarkdown, parseTasksMarkdown, buildTaskPatchOperations, buildNewTaskFields, getTasksFilePath, updateTasksFileAfterCreate,
|
|
17
|
-
// New work item utilities
|
|
18
|
-
parseNewWorkItemMarkdown, buildNewWorkItemFields, generateNewWorkItemTemplate, convertNewFileToSynced, getNewWorkItemFilePath, findNextNewFileIndex, findNewWorkItemFiles, renameFile,
|
|
19
|
-
// Markdown format utilities
|
|
20
|
-
getAllLargeTextFields, autoConvertFieldsToMarkdown, isHtmlContent, } from './sync/index.js';
|
|
12
|
+
import { AzureDevOpsClient } from './azure-devops-client.js';
|
|
13
|
+
import { WikiService, WorkItemService, PullRequestService, BuildService, VariableGroupService, SyncService, ConfigurationService, } from './services/index.js';
|
|
14
|
+
import { registerAllTools } from './tools/index.js';
|
|
15
|
+
import { registerAllPrompts } from './prompts/index.js';
|
|
21
16
|
/**
|
|
22
|
-
*
|
|
23
|
-
* @param server - The MCP server instance
|
|
24
|
-
* @param azuredevopsService - Optional pre-configured AzureDevOpsService (for testing or custom configs)
|
|
17
|
+
* Build a ServiceContext from environment variables (lazy client initialization).
|
|
25
18
|
*/
|
|
26
|
-
|
|
27
|
-
let
|
|
28
|
-
function
|
|
29
|
-
if (!
|
|
19
|
+
function createServiceContext() {
|
|
20
|
+
let client = null;
|
|
21
|
+
function getClient() {
|
|
22
|
+
if (!client) {
|
|
30
23
|
const missingConfig = [];
|
|
31
24
|
if (!process.env.AZUREDEVOPS_ORGANIZATION)
|
|
32
25
|
missingConfig.push("AZUREDEVOPS_ORGANIZATION");
|
|
@@ -48,1874 +41,43 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
48
41
|
enableWikiWrite: process.env.AZUREDEVOPS_ENABLE_WIKI_WRITE === "true",
|
|
49
42
|
enableWikiDelete: process.env.AZUREDEVOPS_ENABLE_WIKI_DELETE === "true",
|
|
50
43
|
enablePullRequestWrite: process.env.AZUREDEVOPS_ENABLE_PR_WRITE === "true",
|
|
51
|
-
// Comment format: 'markdown' (default) or 'html' (for legacy orgs without Markdown preview)
|
|
52
44
|
commentFormat: process.env.AZUREDEVOPS_COMMENT_FORMAT || 'markdown',
|
|
53
45
|
};
|
|
54
|
-
|
|
46
|
+
client = new AzureDevOpsClient(config);
|
|
55
47
|
console.error("Azure DevOps service initialized");
|
|
56
48
|
}
|
|
57
|
-
return
|
|
49
|
+
return client;
|
|
58
50
|
}
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
result.results.forEach((item, index) => {
|
|
78
|
-
report += `### ${index + 1}. ${item.fileName}\n`;
|
|
79
|
-
report += `- **Path:** ${item.path}\n`;
|
|
80
|
-
report += `- **Wiki:** ${item.wikiName}\n`;
|
|
81
|
-
report += `- **Project:** ${item.project}\n`;
|
|
82
|
-
if (item.highlights && item.highlights.length > 0) {
|
|
83
|
-
report += `- **Highlights:**\n`;
|
|
84
|
-
item.highlights.forEach((highlight) => {
|
|
85
|
-
// Remove HTML tags for cleaner display
|
|
86
|
-
const cleanHighlight = highlight.replace(/<[^>]*>/g, '');
|
|
87
|
-
report += ` - ${cleanHighlight}\n`;
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
report += `\n`;
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
report += `No results found for "${searchText}".\n`;
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
messages: [
|
|
98
|
-
{
|
|
99
|
-
role: "assistant",
|
|
100
|
-
content: {
|
|
101
|
-
type: "text",
|
|
102
|
-
text: report
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
]
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
console.error(`Error generating wiki search results:`, error);
|
|
110
|
-
return {
|
|
111
|
-
messages: [
|
|
112
|
-
{
|
|
113
|
-
role: "assistant",
|
|
114
|
-
content: {
|
|
115
|
-
type: "text",
|
|
116
|
-
text: `Error: ${error.message}`
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
]
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
server.prompt("wiki-page-content", "Get a formatted wiki page with navigation context from Azure DevOps", {
|
|
124
|
-
project: z.string().describe("The project name"),
|
|
125
|
-
wikiId: z.string().describe("The wiki identifier"),
|
|
126
|
-
pagePath: z.string().describe("The path to the page"),
|
|
127
|
-
}, async (args) => {
|
|
128
|
-
try {
|
|
129
|
-
const service = getAzureDevOpsService();
|
|
130
|
-
const { project, wikiId, pagePath } = args;
|
|
131
|
-
const result = await service.getWikiPage(project, wikiId, pagePath, true);
|
|
132
|
-
let report = `# Wiki Page: ${pagePath}\n\n`;
|
|
133
|
-
report += `**Project:** ${project}\n`;
|
|
134
|
-
report += `**Wiki:** ${wikiId}\n`;
|
|
135
|
-
report += `**Git Path:** ${result.gitItemPath || 'N/A'}\n\n`;
|
|
136
|
-
if (result.subPages && result.subPages.length > 0) {
|
|
137
|
-
report += `## Sub-pages\n`;
|
|
138
|
-
result.subPages.forEach((subPage) => {
|
|
139
|
-
report += `- ${subPage.path}\n`;
|
|
140
|
-
});
|
|
141
|
-
report += `\n`;
|
|
142
|
-
}
|
|
143
|
-
report += `## Content\n\n`;
|
|
144
|
-
report += result.content || '*No content available*';
|
|
145
|
-
return {
|
|
146
|
-
messages: [
|
|
147
|
-
{
|
|
148
|
-
role: "assistant",
|
|
149
|
-
content: {
|
|
150
|
-
type: "text",
|
|
151
|
-
text: report
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
]
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
console.error(`Error generating wiki page content:`, error);
|
|
159
|
-
return {
|
|
160
|
-
messages: [
|
|
161
|
-
{
|
|
162
|
-
role: "assistant",
|
|
163
|
-
content: {
|
|
164
|
-
type: "text",
|
|
165
|
-
text: `Error: ${error.message}`
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
]
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
server.prompt("work-item-summary", "Get a comprehensive summary of a work item with comments from Azure DevOps", {
|
|
173
|
-
project: z.string().describe("The project name"),
|
|
174
|
-
workItemId: z.string().describe("The work item ID"),
|
|
175
|
-
}, async (args) => {
|
|
176
|
-
try {
|
|
177
|
-
const service = getAzureDevOpsService();
|
|
178
|
-
const { project, workItemId } = args;
|
|
179
|
-
const workItemIdNum = parseInt(workItemId, 10);
|
|
180
|
-
// Get work item and comments in parallel
|
|
181
|
-
const [workItem, comments] = await Promise.all([
|
|
182
|
-
service.getWorkItem(project, workItemIdNum),
|
|
183
|
-
service.getWorkItemComments(project, workItemIdNum)
|
|
184
|
-
]);
|
|
185
|
-
const fields = workItem.fields || {};
|
|
186
|
-
let report = `# Work Item #${workItemId}: ${fields['System.Title'] || 'Untitled'}\n\n`;
|
|
187
|
-
report += `## Details\n`;
|
|
188
|
-
report += `- **Type:** ${fields['System.WorkItemType'] || 'N/A'}\n`;
|
|
189
|
-
report += `- **State:** ${fields['System.State'] || 'N/A'}\n`;
|
|
190
|
-
report += `- **Assigned To:** ${fields['System.AssignedTo']?.displayName || 'Unassigned'}\n`;
|
|
191
|
-
report += `- **Created By:** ${fields['System.CreatedBy']?.displayName || 'N/A'}\n`;
|
|
192
|
-
report += `- **Created Date:** ${fields['System.CreatedDate'] || 'N/A'}\n`;
|
|
193
|
-
report += `- **Changed Date:** ${fields['System.ChangedDate'] || 'N/A'}\n`;
|
|
194
|
-
report += `- **Area Path:** ${fields['System.AreaPath'] || 'N/A'}\n`;
|
|
195
|
-
report += `- **Iteration Path:** ${fields['System.IterationPath'] || 'N/A'}\n`;
|
|
196
|
-
if (fields['System.Tags']) {
|
|
197
|
-
report += `- **Tags:** ${fields['System.Tags']}\n`;
|
|
198
|
-
}
|
|
199
|
-
report += `\n`;
|
|
200
|
-
if (fields['System.Description']) {
|
|
201
|
-
report += `## Description\n${fields['System.Description']}\n\n`;
|
|
202
|
-
}
|
|
203
|
-
if (fields['Microsoft.VSTS.TCM.ReproSteps']) {
|
|
204
|
-
report += `## Repro Steps\n${fields['Microsoft.VSTS.TCM.ReproSteps']}\n\n`;
|
|
205
|
-
}
|
|
206
|
-
if (workItem.relations && workItem.relations.length > 0) {
|
|
207
|
-
report += `## Related Items\n`;
|
|
208
|
-
workItem.relations.forEach((relation) => {
|
|
209
|
-
report += `- ${relation.rel}: ${relation.url}\n`;
|
|
210
|
-
});
|
|
211
|
-
report += `\n`;
|
|
212
|
-
}
|
|
213
|
-
if (comments.comments && comments.comments.length > 0) {
|
|
214
|
-
report += `## Comments (${comments.totalCount})\n\n`;
|
|
215
|
-
comments.comments.forEach((comment) => {
|
|
216
|
-
report += `### ${comment.createdBy} - ${new Date(comment.createdDate).toLocaleString()}\n`;
|
|
217
|
-
report += `${comment.text}\n\n`;
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
return {
|
|
221
|
-
messages: [
|
|
222
|
-
{
|
|
223
|
-
role: "assistant",
|
|
224
|
-
content: {
|
|
225
|
-
type: "text",
|
|
226
|
-
text: report
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
]
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
catch (error) {
|
|
233
|
-
console.error(`Error generating work item summary:`, error);
|
|
234
|
-
return {
|
|
235
|
-
messages: [
|
|
236
|
-
{
|
|
237
|
-
role: "assistant",
|
|
238
|
-
content: {
|
|
239
|
-
type: "text",
|
|
240
|
-
text: `Error: ${error.message}`
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
]
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
server.prompt("work-items-query-report", "Execute a WIQL query and get formatted results grouped by state/type", {
|
|
248
|
-
project: z.string().describe("The project name"),
|
|
249
|
-
wiql: z.string().describe("The WIQL query string"),
|
|
250
|
-
maxResults: z.string().optional().describe("Maximum number of results (default: 200)"),
|
|
251
|
-
}, async (args) => {
|
|
252
|
-
try {
|
|
253
|
-
const service = getAzureDevOpsService();
|
|
254
|
-
const { project, wiql, maxResults } = args;
|
|
255
|
-
const maxResultsNum = maxResults ? parseInt(maxResults, 10) : undefined;
|
|
256
|
-
const result = await service.queryWorkItems(project, wiql, maxResultsNum);
|
|
257
|
-
let report = `# Work Items Query Results\n\n`;
|
|
258
|
-
report += `**Project:** ${project}\n`;
|
|
259
|
-
report += `**Total Results:** ${result.totalCount}\n\n`;
|
|
260
|
-
if (result.workItems && result.workItems.length > 0) {
|
|
261
|
-
// Group by state
|
|
262
|
-
const groupedByState = new Map();
|
|
263
|
-
result.workItems.forEach((item) => {
|
|
264
|
-
const state = item.fields['System.State'] || 'Unknown';
|
|
265
|
-
if (!groupedByState.has(state)) {
|
|
266
|
-
groupedByState.set(state, []);
|
|
267
|
-
}
|
|
268
|
-
groupedByState.get(state).push(item);
|
|
269
|
-
});
|
|
270
|
-
// Sort states: Active, Resolved, Closed, others
|
|
271
|
-
const stateOrder = ['Active', 'New', 'Resolved', 'Closed'];
|
|
272
|
-
const sortedStates = Array.from(groupedByState.keys()).sort((a, b) => {
|
|
273
|
-
const aIndex = stateOrder.indexOf(a);
|
|
274
|
-
const bIndex = stateOrder.indexOf(b);
|
|
275
|
-
if (aIndex === -1 && bIndex === -1)
|
|
276
|
-
return a.localeCompare(b);
|
|
277
|
-
if (aIndex === -1)
|
|
278
|
-
return 1;
|
|
279
|
-
if (bIndex === -1)
|
|
280
|
-
return -1;
|
|
281
|
-
return aIndex - bIndex;
|
|
282
|
-
});
|
|
283
|
-
sortedStates.forEach(state => {
|
|
284
|
-
const items = groupedByState.get(state);
|
|
285
|
-
report += `## ${state} (${items.length})\n\n`;
|
|
286
|
-
items.forEach((item) => {
|
|
287
|
-
const fields = item.fields;
|
|
288
|
-
report += `- **#${item.id}**: ${fields['System.Title'] || 'Untitled'}\n`;
|
|
289
|
-
report += ` - Type: ${fields['System.WorkItemType'] || 'N/A'}`;
|
|
290
|
-
report += `, Assigned: ${fields['System.AssignedTo']?.displayName || 'Unassigned'}\n`;
|
|
291
|
-
});
|
|
292
|
-
report += `\n`;
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
report += `No work items found matching the query.\n`;
|
|
297
|
-
}
|
|
298
|
-
return {
|
|
299
|
-
messages: [
|
|
300
|
-
{
|
|
301
|
-
role: "assistant",
|
|
302
|
-
content: {
|
|
303
|
-
type: "text",
|
|
304
|
-
text: report
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
]
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
catch (error) {
|
|
311
|
-
console.error(`Error generating work items query report:`, error);
|
|
312
|
-
return {
|
|
313
|
-
messages: [
|
|
314
|
-
{
|
|
315
|
-
role: "assistant",
|
|
316
|
-
content: {
|
|
317
|
-
type: "text",
|
|
318
|
-
text: `Error: ${error.message}`
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
]
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
// ========================================
|
|
326
|
-
// TOOLS
|
|
327
|
-
// ========================================
|
|
328
|
-
server.tool("get-configuration", "Get the configured Azure DevOps organization and projects. Use this to construct correct URLs.", {}, async () => {
|
|
329
|
-
try {
|
|
330
|
-
const organization = process.env.AZUREDEVOPS_ORGANIZATION;
|
|
331
|
-
const projects = process.env.AZUREDEVOPS_PROJECTS?.split(",").map(p => p.trim()).filter(p => p) || [];
|
|
332
|
-
const syncFolder = process.env.AZUREDEVOPS_SYNC_FOLDER || 'docs/user-stories';
|
|
333
|
-
if (!organization || projects.length === 0) {
|
|
334
|
-
return {
|
|
335
|
-
content: [{
|
|
336
|
-
type: "text",
|
|
337
|
-
text: "Azure DevOps not configured. Set AZUREDEVOPS_ORGANIZATION and AZUREDEVOPS_PROJECTS environment variables.",
|
|
338
|
-
}],
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
const config = {
|
|
342
|
-
organization,
|
|
343
|
-
projects,
|
|
344
|
-
syncFolder,
|
|
345
|
-
urlPatterns: {
|
|
346
|
-
workItem: `https://dev.azure.com/${organization}/{project}/_workitems/edit/{id}`,
|
|
347
|
-
pullRequest: `https://dev.azure.com/${organization}/{project}/_git/{repo}/pullrequest/{id}`,
|
|
348
|
-
wiki: `https://dev.azure.com/${organization}/{project}/_wiki/wikis/{wikiName}/{pagePath}`,
|
|
349
|
-
},
|
|
350
|
-
};
|
|
351
|
-
return {
|
|
352
|
-
content: [{
|
|
353
|
-
type: "text",
|
|
354
|
-
text: `Azure DevOps Configuration:\n\n${JSON.stringify(config, null, 2)}`,
|
|
355
|
-
}],
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
catch (error) {
|
|
359
|
-
return {
|
|
360
|
-
content: [{
|
|
361
|
-
type: "text",
|
|
362
|
-
text: `Failed to get configuration: ${error.message}`,
|
|
363
|
-
}],
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
server.tool("get-wikis", "Get all wikis in an Azure DevOps project", {
|
|
368
|
-
project: z.string().describe("The project name"),
|
|
369
|
-
}, async ({ project }) => {
|
|
370
|
-
try {
|
|
371
|
-
const service = getAzureDevOpsService();
|
|
372
|
-
const result = await service.getWikis(project);
|
|
373
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
374
|
-
return {
|
|
375
|
-
content: [
|
|
376
|
-
{
|
|
377
|
-
type: "text",
|
|
378
|
-
text: `Wikis in project '${project}':\n\n${resultStr}`,
|
|
379
|
-
},
|
|
380
|
-
],
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
catch (error) {
|
|
384
|
-
console.error("Error getting wikis:", error);
|
|
385
|
-
return {
|
|
386
|
-
content: [
|
|
387
|
-
{
|
|
388
|
-
type: "text",
|
|
389
|
-
text: `Failed to get wikis: ${error.message}`,
|
|
390
|
-
},
|
|
391
|
-
],
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
server.tool("search-wiki-pages", "Search wiki pages across Azure DevOps projects", {
|
|
396
|
-
searchText: z.string().describe("The text to search for"),
|
|
397
|
-
project: z.string().optional().describe("Optional project filter"),
|
|
398
|
-
maxResults: z.number().optional().describe("Maximum number of results (default: 25)"),
|
|
399
|
-
}, async ({ searchText, project, maxResults }) => {
|
|
400
|
-
try {
|
|
401
|
-
const service = getAzureDevOpsService();
|
|
402
|
-
const result = await service.searchWikiPages(searchText, project, maxResults);
|
|
403
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
404
|
-
return {
|
|
405
|
-
content: [
|
|
406
|
-
{
|
|
407
|
-
type: "text",
|
|
408
|
-
text: `Wiki search results for '${searchText}':\n\n${resultStr}`,
|
|
409
|
-
},
|
|
410
|
-
],
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
catch (error) {
|
|
414
|
-
console.error("Error searching wiki pages:", error);
|
|
415
|
-
return {
|
|
416
|
-
content: [
|
|
417
|
-
{
|
|
418
|
-
type: "text",
|
|
419
|
-
text: `Failed to search wiki pages: ${error.message}`,
|
|
420
|
-
},
|
|
421
|
-
],
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
server.tool("get-wiki-page", "Get a specific wiki page with content from Azure DevOps", {
|
|
426
|
-
project: z.string().describe("The project name"),
|
|
427
|
-
wikiId: z.string().describe("The wiki identifier (ID or name)"),
|
|
428
|
-
pagePath: z.string().describe("The path to the page (e.g., '/Setup/Authentication')"),
|
|
429
|
-
includeContent: z.boolean().optional().describe("Include page content (default: true)"),
|
|
430
|
-
}, async ({ project, wikiId, pagePath, includeContent }) => {
|
|
431
|
-
try {
|
|
432
|
-
const service = getAzureDevOpsService();
|
|
433
|
-
const result = await service.getWikiPage(project, wikiId, pagePath, includeContent ?? true);
|
|
434
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
435
|
-
return {
|
|
436
|
-
content: [
|
|
437
|
-
{
|
|
438
|
-
type: "text",
|
|
439
|
-
text: `Wiki page '${pagePath}':\n\n${resultStr}`,
|
|
440
|
-
},
|
|
441
|
-
],
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
catch (error) {
|
|
445
|
-
console.error("Error getting wiki page:", error);
|
|
446
|
-
return {
|
|
447
|
-
content: [
|
|
448
|
-
{
|
|
449
|
-
type: "text",
|
|
450
|
-
text: `Failed to get wiki page: ${error.message}`,
|
|
451
|
-
},
|
|
452
|
-
],
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
server.tool("create-wiki-page", "Create a new wiki page in Azure DevOps (requires AZUREDEVOPS_ENABLE_WIKI_WRITE=true)", {
|
|
457
|
-
project: z.string().describe("The project name"),
|
|
458
|
-
wikiId: z.string().describe("The wiki identifier"),
|
|
459
|
-
pagePath: z.string().describe("The path for the new page (e.g., '/Setup/NewGuide')"),
|
|
460
|
-
content: z.string().describe("The markdown content for the page"),
|
|
461
|
-
}, async ({ project, wikiId, pagePath, content }) => {
|
|
462
|
-
try {
|
|
463
|
-
const service = getAzureDevOpsService();
|
|
464
|
-
const result = await service.createWikiPage(project, wikiId, pagePath, content);
|
|
465
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
466
|
-
return {
|
|
467
|
-
content: [
|
|
468
|
-
{
|
|
469
|
-
type: "text",
|
|
470
|
-
text: `Created wiki page '${pagePath}':\n\n${resultStr}`,
|
|
471
|
-
},
|
|
472
|
-
],
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
catch (error) {
|
|
476
|
-
console.error("Error creating wiki page:", error);
|
|
477
|
-
return {
|
|
478
|
-
content: [
|
|
479
|
-
{
|
|
480
|
-
type: "text",
|
|
481
|
-
text: `Failed to create wiki page: ${error.message}`,
|
|
482
|
-
},
|
|
483
|
-
],
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
server.tool("update-wiki-page", "Update an existing wiki page in Azure DevOps. Version is auto-fetched if not provided, so you can update pages without first calling get-wiki-page. (requires AZUREDEVOPS_ENABLE_WIKI_WRITE=true)", {
|
|
488
|
-
project: z.string().describe("The project name"),
|
|
489
|
-
wikiId: z.string().describe("The wiki identifier"),
|
|
490
|
-
pagePath: z.string().describe("The path to the page"),
|
|
491
|
-
content: z.string().describe("The updated markdown content"),
|
|
492
|
-
version: z.string().optional().describe("The ETag/version for optimistic concurrency. If not provided, will be auto-fetched from the current page."),
|
|
493
|
-
}, async ({ project, wikiId, pagePath, content, version }) => {
|
|
494
|
-
try {
|
|
495
|
-
const service = getAzureDevOpsService();
|
|
496
|
-
const result = await service.updateWikiPage(project, wikiId, pagePath, content, version);
|
|
497
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
498
|
-
return {
|
|
499
|
-
content: [
|
|
500
|
-
{
|
|
501
|
-
type: "text",
|
|
502
|
-
text: `Updated wiki page '${pagePath}':\n\n${resultStr}`,
|
|
503
|
-
},
|
|
504
|
-
],
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
catch (error) {
|
|
508
|
-
console.error("Error updating wiki page:", error);
|
|
509
|
-
return {
|
|
510
|
-
content: [
|
|
511
|
-
{
|
|
512
|
-
type: "text",
|
|
513
|
-
text: `Failed to update wiki page: ${error.message}`,
|
|
514
|
-
},
|
|
515
|
-
],
|
|
516
|
-
isError: true,
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
server.tool("ado-str-replace-wiki", "Replace a specific string in an Azure DevOps wiki page without rewriting entire content. More efficient than update-wiki-page for small changes. (requires AZUREDEVOPS_ENABLE_WIKI_WRITE=true)", {
|
|
521
|
-
project: z.string().describe("The project name"),
|
|
522
|
-
wikiId: z.string().describe("The wiki identifier (ID or name)"),
|
|
523
|
-
pagePath: z.string().describe("The path to the wiki page (e.g., '/SharePoint-Online/04-DEV-Configuration')"),
|
|
524
|
-
old_str: z.string().describe("The exact string to replace (must be unique unless replace_all is true)"),
|
|
525
|
-
new_str: z.string().describe("The replacement string"),
|
|
526
|
-
replace_all: z.boolean().optional().describe("If true, replace all occurrences. If false (default), old_str must be unique in the page."),
|
|
527
|
-
description: z.string().optional().describe("Optional description of the change (for audit logging)")
|
|
528
|
-
}, async ({ project, wikiId, pagePath, old_str, new_str, replace_all, description }) => {
|
|
529
|
-
try {
|
|
530
|
-
const service = getAzureDevOpsService();
|
|
531
|
-
const result = await service.strReplaceWikiPage(project, wikiId, pagePath, old_str, new_str, replace_all ?? false, description);
|
|
532
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
533
|
-
return {
|
|
534
|
-
content: [
|
|
535
|
-
{
|
|
536
|
-
type: "text",
|
|
537
|
-
text: `Successfully replaced "${old_str}" with "${new_str}" in wiki page '${pagePath}' (${result.occurrences} occurrence(s)):\n\n${resultStr}\n\nDiff:\n${result.diff}`,
|
|
538
|
-
},
|
|
539
|
-
],
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
catch (error) {
|
|
543
|
-
console.error("Error replacing text in wiki page:", error);
|
|
544
|
-
return {
|
|
545
|
-
content: [
|
|
546
|
-
{
|
|
547
|
-
type: "text",
|
|
548
|
-
text: `Failed to replace text in wiki page: ${error.message}`,
|
|
549
|
-
},
|
|
550
|
-
],
|
|
551
|
-
isError: true,
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
server.tool("delete-wiki-page", "Delete a wiki page permanently (requires AZUREDEVOPS_ENABLE_WIKI_DELETE=true). WARNING: This permanently deletes the page and all sub-pages.", {
|
|
556
|
-
project: z.string().describe("The project name"),
|
|
557
|
-
wikiId: z.string().describe("The wiki identifier"),
|
|
558
|
-
pagePath: z.string().describe("Page path (e.g., '/Setup/Old-Page')"),
|
|
559
|
-
}, async ({ project, wikiId, pagePath }) => {
|
|
560
|
-
try {
|
|
561
|
-
const service = getAzureDevOpsService();
|
|
562
|
-
const result = await service.deleteWikiPage(project, wikiId, pagePath);
|
|
563
|
-
return { content: [{ type: "text", text: `Deleted wiki page:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
564
|
-
}
|
|
565
|
-
catch (error) {
|
|
566
|
-
console.error("Error deleting wiki page:", error);
|
|
567
|
-
return { content: [{ type: "text", text: `Failed to delete wiki page: ${error.message}` }] };
|
|
568
|
-
}
|
|
569
|
-
});
|
|
570
|
-
server.tool("get-work-item", "Get a work item by ID with full details from Azure DevOps", {
|
|
571
|
-
project: z.string().describe("The project name"),
|
|
572
|
-
workItemId: z.number().describe("The work item ID"),
|
|
573
|
-
}, async ({ project, workItemId }) => {
|
|
574
|
-
try {
|
|
575
|
-
const service = getAzureDevOpsService();
|
|
576
|
-
const result = await service.getWorkItem(project, workItemId);
|
|
577
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
578
|
-
return {
|
|
579
|
-
content: [
|
|
580
|
-
{
|
|
581
|
-
type: "text",
|
|
582
|
-
text: `Work item ${workItemId}:\n\n${resultStr}`,
|
|
583
|
-
},
|
|
584
|
-
],
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
catch (error) {
|
|
588
|
-
console.error("Error getting work item:", error);
|
|
589
|
-
return {
|
|
590
|
-
content: [
|
|
591
|
-
{
|
|
592
|
-
type: "text",
|
|
593
|
-
text: `Failed to get work item: ${error.message}`,
|
|
594
|
-
},
|
|
595
|
-
],
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
|
-
server.tool("query-work-items", "Query work items using WIQL (Work Item Query Language) in Azure DevOps", {
|
|
600
|
-
project: z.string().describe("The project name"),
|
|
601
|
-
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)),
|
|
602
|
-
maxResults: z.number().optional().describe("Maximum number of results (default: 200)"),
|
|
603
|
-
}, async ({ project, wiql, maxResults }) => {
|
|
604
|
-
try {
|
|
605
|
-
const service = getAzureDevOpsService();
|
|
606
|
-
const result = await service.queryWorkItems(project, wiql, maxResults);
|
|
607
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
608
|
-
return {
|
|
609
|
-
content: [
|
|
610
|
-
{
|
|
611
|
-
type: "text",
|
|
612
|
-
text: `Work items query results:\n\n${resultStr}`,
|
|
613
|
-
},
|
|
614
|
-
],
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
catch (error) {
|
|
618
|
-
console.error("Error querying work items:", error);
|
|
619
|
-
return {
|
|
620
|
-
content: [
|
|
621
|
-
{
|
|
622
|
-
type: "text",
|
|
623
|
-
text: `Failed to query work items: ${error.message}`,
|
|
624
|
-
},
|
|
625
|
-
],
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
server.tool("run-saved-query", "Execute a saved Azure DevOps query by its query ID (GUID). Returns a compact summary by default (ID, Title, Assigned To, State, Severity, Priority, Tags, Story Points, Resolved Reason). Use detail='full' for all fields.", {
|
|
630
|
-
project: z.string().describe("The project name"),
|
|
631
|
-
queryId: z.string().describe(descWithExamples("The saved query GUID. Found in ADO query URLs: https://dev.azure.com/{org}/{project}/_queries/query/{queryId}/", [
|
|
632
|
-
{ label: "From URL", value: "f8c22439-f72d-4018-b863-a52396b26c4b" },
|
|
633
|
-
])),
|
|
634
|
-
maxResults: z.number().optional().describe("Maximum number of results (default: 50)"),
|
|
635
|
-
detail: z.enum(['summary', 'full']).optional().describe("Level of detail: 'summary' (default) returns key fields only, 'full' returns all fields expanded"),
|
|
636
|
-
fields: z.array(z.string()).optional().describe(descWithExamples("Custom list of ADO field reference names to return. Overrides the default summary fields.", [
|
|
637
|
-
{ label: "Effort and state", value: '["System.Title", "System.State", "Microsoft.VSTS.Scheduling.Effort"]' },
|
|
638
|
-
])),
|
|
639
|
-
groupBy: z.string().optional().describe(descWithExamples("Group results by a summary field name and return counts per group.", [
|
|
640
|
-
{ label: "By state", value: "state" },
|
|
641
|
-
{ label: "By assigned to", value: "assignedTo" },
|
|
642
|
-
{ label: "By priority", value: "priority" },
|
|
643
|
-
])),
|
|
644
|
-
}, async ({ project, queryId, maxResults, detail, fields, groupBy }) => {
|
|
645
|
-
try {
|
|
646
|
-
const service = getAzureDevOpsService();
|
|
647
|
-
const result = await service.runSavedQuery(project, queryId, maxResults, detail, fields, groupBy);
|
|
648
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
649
|
-
return {
|
|
650
|
-
content: [
|
|
651
|
-
{
|
|
652
|
-
type: "text",
|
|
653
|
-
text: `Saved query results:\n\n${resultStr}`,
|
|
654
|
-
},
|
|
655
|
-
],
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
catch (error) {
|
|
659
|
-
console.error("Error running saved query:", error);
|
|
660
|
-
return {
|
|
661
|
-
content: [
|
|
662
|
-
{
|
|
663
|
-
type: "text",
|
|
664
|
-
text: `Failed to run saved query: ${error.message}`,
|
|
665
|
-
},
|
|
666
|
-
],
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
});
|
|
670
|
-
server.tool("get-saved-query", "Get a saved Azure DevOps query's metadata and WIQL text without executing it. Useful for inspecting or modifying a query before running it.", {
|
|
671
|
-
project: z.string().describe("The project name"),
|
|
672
|
-
queryId: z.string().describe("The saved query GUID"),
|
|
673
|
-
}, async ({ project, queryId }) => {
|
|
674
|
-
try {
|
|
675
|
-
const service = getAzureDevOpsService();
|
|
676
|
-
const result = await service.getSavedQuery(project, queryId);
|
|
677
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
678
|
-
return {
|
|
679
|
-
content: [
|
|
680
|
-
{
|
|
681
|
-
type: "text",
|
|
682
|
-
text: `Saved query details:\n\n${resultStr}`,
|
|
683
|
-
},
|
|
684
|
-
],
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
catch (error) {
|
|
688
|
-
console.error("Error getting saved query:", error);
|
|
689
|
-
return {
|
|
690
|
-
content: [
|
|
691
|
-
{
|
|
692
|
-
type: "text",
|
|
693
|
-
text: `Failed to get saved query: ${error.message}`,
|
|
694
|
-
},
|
|
695
|
-
],
|
|
696
|
-
};
|
|
697
|
-
}
|
|
698
|
-
});
|
|
699
|
-
server.tool("get-work-item-comments", "Get comments/discussion for a work item in Azure DevOps", {
|
|
700
|
-
project: z.string().describe("The project name"),
|
|
701
|
-
workItemId: z.number().describe("The work item ID"),
|
|
702
|
-
}, async ({ project, workItemId }) => {
|
|
703
|
-
try {
|
|
704
|
-
const service = getAzureDevOpsService();
|
|
705
|
-
const result = await service.getWorkItemComments(project, workItemId);
|
|
706
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
707
|
-
return {
|
|
708
|
-
content: [
|
|
709
|
-
{
|
|
710
|
-
type: "text",
|
|
711
|
-
text: `Comments for work item ${workItemId}:\n\n${resultStr}`,
|
|
712
|
-
},
|
|
713
|
-
],
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
catch (error) {
|
|
717
|
-
console.error("Error getting work item comments:", error);
|
|
718
|
-
return {
|
|
719
|
-
content: [
|
|
720
|
-
{
|
|
721
|
-
type: "text",
|
|
722
|
-
text: `Failed to get work item comments: ${error.message}`,
|
|
723
|
-
},
|
|
724
|
-
],
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
server.tool("add-work-item-comment", "Add a comment to a work item in Azure DevOps (requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true). By default, comments are sent as Markdown. For orgs without Markdown preview, set AZUREDEVOPS_COMMENT_FORMAT=html to auto-convert Markdown to HTML.", {
|
|
729
|
-
project: z.string().describe("The project name"),
|
|
730
|
-
workItemId: z.number().describe("The work item ID"),
|
|
731
|
-
commentText: z.string().describe("The comment text in Markdown format. Use standard Markdown syntax: **bold**, *italic*, `code`, - lists, [links](url), etc. Will be auto-converted to HTML if AZUREDEVOPS_COMMENT_FORMAT=html is set."),
|
|
732
|
-
}, async ({ project, workItemId, commentText }) => {
|
|
733
|
-
try {
|
|
734
|
-
const service = getAzureDevOpsService();
|
|
735
|
-
const result = await service.addWorkItemComment(project, workItemId, commentText);
|
|
736
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
737
|
-
return {
|
|
738
|
-
content: [
|
|
739
|
-
{
|
|
740
|
-
type: "text",
|
|
741
|
-
text: `Added comment to work item ${workItemId}:\n\n${resultStr}`,
|
|
742
|
-
},
|
|
743
|
-
],
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
catch (error) {
|
|
747
|
-
console.error("Error adding work item comment:", error);
|
|
748
|
-
return {
|
|
749
|
-
content: [
|
|
750
|
-
{
|
|
751
|
-
type: "text",
|
|
752
|
-
text: `Failed to add work item comment: ${error.message}`,
|
|
753
|
-
},
|
|
754
|
-
],
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
});
|
|
758
|
-
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.", {
|
|
759
|
-
project: z.string().describe("The project name"),
|
|
760
|
-
workItemId: z.number().describe("The work item ID"),
|
|
761
|
-
patchOperations: z.array(z.object({
|
|
762
|
-
op: z.string().describe("The operation type: 'add' (set value), 'replace' (update existing), or 'remove' (clear field)"),
|
|
763
|
-
path: z.string().describe("The field path starting with '/fields/' (e.g., '/fields/System.State', '/fields/System.Title')"),
|
|
764
|
-
value: z.any().optional().describe("The value to set (not required for 'remove' operation)")
|
|
765
|
-
})).describe(descWithExamples("Array of JSON Patch operations. Each operation specifies what to change.", PATCH_OP_EXAMPLES)),
|
|
766
|
-
skipAutoConvert: z.boolean().optional().describe("Skip automatic markdown format injection for large text fields. Only use when explicitly requested. Default: false"),
|
|
767
|
-
}, async ({ project, workItemId, patchOperations, skipAutoConvert }) => {
|
|
768
|
-
try {
|
|
769
|
-
const service = getAzureDevOpsService();
|
|
770
|
-
// Auto-inject markdown format operations for large text fields
|
|
771
|
-
let finalOperations = [...patchOperations];
|
|
772
|
-
const formatOpsAdded = [];
|
|
773
|
-
if (!skipAutoConvert) {
|
|
774
|
-
// Find large text fields being updated (includes custom fields)
|
|
775
|
-
const allLargeTextFields = getAllLargeTextFields();
|
|
776
|
-
const largeTextFieldsBeingUpdated = patchOperations
|
|
777
|
-
.filter((op) => op.path?.startsWith('/fields/'))
|
|
778
|
-
.map((op) => op.path.replace('/fields/', ''))
|
|
779
|
-
.filter((field) => allLargeTextFields.includes(field));
|
|
780
|
-
for (const field of largeTextFieldsBeingUpdated) {
|
|
781
|
-
// Check if format operation already exists
|
|
782
|
-
const hasFormatOp = finalOperations.some((op) => op.path === `/multilineFieldsFormat/${field}`);
|
|
783
|
-
if (!hasFormatOp) {
|
|
784
|
-
// Add format operation to ensure markdown
|
|
785
|
-
finalOperations.push({
|
|
786
|
-
op: 'add',
|
|
787
|
-
path: `/multilineFieldsFormat/${field}`,
|
|
788
|
-
value: 'Markdown'
|
|
789
|
-
});
|
|
790
|
-
formatOpsAdded.push(field);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
const result = await service.updateWorkItem(project, workItemId, finalOperations);
|
|
795
|
-
let message = `Updated work item ${workItemId}`;
|
|
796
|
-
if (formatOpsAdded.length > 0) {
|
|
797
|
-
message += ` (auto-set markdown format for: ${formatOpsAdded.join(', ')})`;
|
|
798
|
-
}
|
|
799
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
800
|
-
return {
|
|
801
|
-
content: [
|
|
802
|
-
{
|
|
803
|
-
type: "text",
|
|
804
|
-
text: `${message}:\n\n${resultStr}`,
|
|
805
|
-
},
|
|
806
|
-
],
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
catch (error) {
|
|
810
|
-
console.error("Error updating work item:", error);
|
|
811
|
-
return {
|
|
812
|
-
content: [
|
|
813
|
-
{
|
|
814
|
-
type: "text",
|
|
815
|
-
text: `Failed to update work item: ${error.message}`,
|
|
816
|
-
},
|
|
817
|
-
],
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
});
|
|
821
|
-
server.tool("create-work-item", "Create a new work item in Azure DevOps with optional parent relationship (requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true)", {
|
|
822
|
-
project: z.string().describe("The project name"),
|
|
823
|
-
workItemType: z.string().describe("The work item type: 'Bug', 'Task', 'User Story', 'Feature', 'Epic', or custom types"),
|
|
824
|
-
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)),
|
|
825
|
-
parentId: z.number().optional().describe("Optional parent work item ID (for creating child items). Simplified alternative to relations parameter."),
|
|
826
|
-
relations: z.array(z.object({
|
|
827
|
-
rel: z.string().describe("Relation type (e.g., 'System.LinkTypes.Hierarchy-Reverse' for parent)"),
|
|
828
|
-
url: z.string().describe("URL to related work item (e.g., 'https://dev.azure.com/org/project/_apis/wit/workItems/123')"),
|
|
829
|
-
attributes: z.record(z.any()).optional().describe("Optional relation attributes")
|
|
830
|
-
})).optional().describe("Optional array of work item relationships. Use parentId for simple parent-child relationships.")
|
|
831
|
-
}, async ({ project, workItemType, fields, parentId, relations }) => {
|
|
832
|
-
try {
|
|
833
|
-
const service = getAzureDevOpsService();
|
|
834
|
-
const result = await service.createWorkItem(project, workItemType, fields, parentId, relations);
|
|
835
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
836
|
-
return {
|
|
837
|
-
content: [
|
|
838
|
-
{
|
|
839
|
-
type: "text",
|
|
840
|
-
text: `Created work item:\n\n${resultStr}`,
|
|
841
|
-
},
|
|
842
|
-
],
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
catch (error) {
|
|
846
|
-
console.error("Error creating work item:", error);
|
|
847
|
-
return {
|
|
848
|
-
content: [
|
|
849
|
-
{
|
|
850
|
-
type: "text",
|
|
851
|
-
text: `Failed to create work item: ${error.message}`,
|
|
852
|
-
},
|
|
853
|
-
],
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
});
|
|
857
|
-
server.tool("delete-work-item", "Delete a work item in Azure DevOps (requires AZUREDEVOPS_ENABLE_WORK_ITEM_DELETE=true)", {
|
|
858
|
-
project: z.string().describe("The project name"),
|
|
859
|
-
workItemId: z.number().describe("The work item ID"),
|
|
860
|
-
}, async ({ project, workItemId }) => {
|
|
861
|
-
try {
|
|
862
|
-
const service = getAzureDevOpsService();
|
|
863
|
-
const result = await service.deleteWorkItem(project, workItemId);
|
|
864
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
865
|
-
return {
|
|
866
|
-
content: [
|
|
867
|
-
{
|
|
868
|
-
type: "text",
|
|
869
|
-
text: `Deleted work item ${workItemId}:\n\n${resultStr}`,
|
|
870
|
-
},
|
|
871
|
-
],
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
catch (error) {
|
|
875
|
-
console.error("Error deleting work item:", error);
|
|
876
|
-
return {
|
|
877
|
-
content: [
|
|
878
|
-
{
|
|
879
|
-
type: "text",
|
|
880
|
-
text: `Failed to delete work item: ${error.message}`,
|
|
881
|
-
},
|
|
882
|
-
],
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
});
|
|
886
|
-
// ========================================
|
|
887
|
-
// PULL REQUEST TOOLS (Read-only - always available)
|
|
888
|
-
// ========================================
|
|
889
|
-
server.tool("list-repositories", "List all Git repositories in an Azure DevOps project. Returns repository ID, name, default branch, and URLs.", {
|
|
890
|
-
project: z.string().describe("The project name"),
|
|
891
|
-
}, async ({ project }) => {
|
|
892
|
-
try {
|
|
893
|
-
const service = getAzureDevOpsService();
|
|
894
|
-
const result = await service.listRepositories(project);
|
|
895
|
-
return { content: [{ type: "text", text: `Repositories in project '${project}':\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
896
|
-
}
|
|
897
|
-
catch (error) {
|
|
898
|
-
console.error("Error listing repositories:", error);
|
|
899
|
-
return { content: [{ type: "text", text: `Failed to list repositories: ${error.message}` }] };
|
|
900
|
-
}
|
|
901
|
-
});
|
|
902
|
-
server.tool("list-pull-requests", "List pull requests in a Git repository. Filter by status (active, completed, abandoned, all). Returns PR ID, title, author, branches, and review status.", {
|
|
903
|
-
project: z.string().describe("The project name"),
|
|
904
|
-
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
905
|
-
status: z.enum(["active", "completed", "abandoned", "all"]).optional().describe("Filter by PR status (default: active)"),
|
|
906
|
-
top: z.number().optional().describe("Maximum results (default: 25)"),
|
|
907
|
-
}, async ({ project, repositoryId, status, top }) => {
|
|
908
|
-
try {
|
|
909
|
-
const service = getAzureDevOpsService();
|
|
910
|
-
const result = await service.listPullRequests(project, repositoryId, status || 'active', top || 25);
|
|
911
|
-
return { content: [{ type: "text", text: `Pull requests in '${repositoryId}':\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
912
|
-
}
|
|
913
|
-
catch (error) {
|
|
914
|
-
console.error("Error listing pull requests:", error);
|
|
915
|
-
return { content: [{ type: "text", text: `Failed to list pull requests: ${error.message}` }] };
|
|
916
|
-
}
|
|
917
|
-
});
|
|
918
|
-
server.tool("get-pull-request", "Get details of a specific pull request including title, description, author, reviewers with votes, and merge status.", {
|
|
919
|
-
project: z.string().describe("The project name"),
|
|
920
|
-
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
921
|
-
pullRequestId: z.number().describe("The pull request ID"),
|
|
922
|
-
}, async ({ project, repositoryId, pullRequestId }) => {
|
|
923
|
-
try {
|
|
924
|
-
const service = getAzureDevOpsService();
|
|
925
|
-
const result = await service.getPullRequest(project, repositoryId, pullRequestId);
|
|
926
|
-
return { content: [{ type: "text", text: `Pull request #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
927
|
-
}
|
|
928
|
-
catch (error) {
|
|
929
|
-
console.error("Error getting pull request:", error);
|
|
930
|
-
return { content: [{ type: "text", text: `Failed to get pull request: ${error.message}` }] };
|
|
931
|
-
}
|
|
932
|
-
});
|
|
933
|
-
server.tool("get-pull-request-threads", "Get all comment threads and discussions on a pull request. Includes inline code comments with file paths and line numbers.", {
|
|
934
|
-
project: z.string().describe("The project name"),
|
|
935
|
-
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
936
|
-
pullRequestId: z.number().describe("The pull request ID"),
|
|
937
|
-
}, async ({ project, repositoryId, pullRequestId }) => {
|
|
938
|
-
try {
|
|
939
|
-
const service = getAzureDevOpsService();
|
|
940
|
-
const result = await service.getPullRequestThreads(project, repositoryId, pullRequestId);
|
|
941
|
-
return { content: [{ type: "text", text: `Threads for PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
942
|
-
}
|
|
943
|
-
catch (error) {
|
|
944
|
-
console.error("Error getting pull request threads:", error);
|
|
945
|
-
return { content: [{ type: "text", text: `Failed to get pull request threads: ${error.message}` }] };
|
|
946
|
-
}
|
|
947
|
-
});
|
|
948
|
-
server.tool("get-pull-request-commits", "Get all commits included in a pull request. Shows commit ID, message, author, and date.", {
|
|
949
|
-
project: z.string().describe("The project name"),
|
|
950
|
-
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
951
|
-
pullRequestId: z.number().describe("The pull request ID"),
|
|
952
|
-
}, async ({ project, repositoryId, pullRequestId }) => {
|
|
953
|
-
try {
|
|
954
|
-
const service = getAzureDevOpsService();
|
|
955
|
-
const result = await service.getPullRequestCommits(project, repositoryId, pullRequestId);
|
|
956
|
-
return { content: [{ type: "text", text: `Commits for PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
957
|
-
}
|
|
958
|
-
catch (error) {
|
|
959
|
-
console.error("Error getting pull request commits:", error);
|
|
960
|
-
return { content: [{ type: "text", text: `Failed to get pull request commits: ${error.message}` }] };
|
|
961
|
-
}
|
|
962
|
-
});
|
|
963
|
-
server.tool("get-pull-request-changes", "Get file changes (diffs) in a pull request. Shows added, modified, deleted, and renamed files with their paths.", {
|
|
964
|
-
project: z.string().describe("The project name"),
|
|
965
|
-
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
966
|
-
pullRequestId: z.number().describe("The pull request ID"),
|
|
967
|
-
iterationId: z.number().optional().describe("Iteration ID (default: latest)"),
|
|
968
|
-
}, async ({ project, repositoryId, pullRequestId, iterationId }) => {
|
|
969
|
-
try {
|
|
970
|
-
const service = getAzureDevOpsService();
|
|
971
|
-
const result = await service.getPullRequestChanges(project, repositoryId, pullRequestId, iterationId);
|
|
972
|
-
return { content: [{ type: "text", text: `Changes for PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
973
|
-
}
|
|
974
|
-
catch (error) {
|
|
975
|
-
console.error("Error getting pull request changes:", error);
|
|
976
|
-
return { content: [{ type: "text", text: `Failed to get pull request changes: ${error.message}` }] };
|
|
977
|
-
}
|
|
978
|
-
});
|
|
979
|
-
// ========================================
|
|
980
|
-
// PULL REQUEST WRITE TOOLS (Conditional)
|
|
981
|
-
// ========================================
|
|
982
|
-
const enablePullRequestWrite = process.env.AZUREDEVOPS_ENABLE_PR_WRITE === "true";
|
|
983
|
-
if (enablePullRequestWrite) {
|
|
984
|
-
server.tool("add-pull-request-thread", "Add a comment or code review feedback to a pull request. Supports both general comments and inline comments on specific files/lines. (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
985
|
-
project: z.string().describe("The project name"),
|
|
986
|
-
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
987
|
-
pullRequestId: z.number().describe("The pull request ID"),
|
|
988
|
-
content: z.string().describe("Comment content (markdown supported)"),
|
|
989
|
-
filePath: z.string().optional().describe("File path for inline comment (e.g., '/src/file.ts')"),
|
|
990
|
-
lineNumber: z.number().optional().describe("Line number for inline comment (right side of diff)"),
|
|
991
|
-
status: z.enum(["active", "fixed", "wontFix", "closed", "byDesign", "pending"]).optional()
|
|
992
|
-
.describe("Thread status (default: active)"),
|
|
993
|
-
}, async ({ project, repositoryId, pullRequestId, content, filePath, lineNumber, status }) => {
|
|
994
|
-
try {
|
|
995
|
-
const service = getAzureDevOpsService();
|
|
996
|
-
const result = await service.addPullRequestThread(project, repositoryId, pullRequestId, content, filePath, lineNumber, status || 'active');
|
|
997
|
-
return { content: [{ type: "text", text: `Added comment to PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
998
|
-
}
|
|
999
|
-
catch (error) {
|
|
1000
|
-
console.error("Error adding pull request thread:", error);
|
|
1001
|
-
return { content: [{ type: "text", text: `Failed to add pull request thread: ${error.message}` }] };
|
|
1002
|
-
}
|
|
1003
|
-
});
|
|
1004
|
-
server.tool("create-pull-request", "Create a new pull request in a Git repository. (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1005
|
-
project: z.string().describe("The project name"),
|
|
1006
|
-
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
1007
|
-
sourceRefName: z.string().describe(descWithExamples("Source branch full ref name", PR_BRANCH_REF_EXAMPLES)),
|
|
1008
|
-
targetRefName: z.string().describe(descWithExamples("Target branch full ref name", PR_BRANCH_REF_EXAMPLES)),
|
|
1009
|
-
title: z.string().describe("Pull request title"),
|
|
1010
|
-
description: z.string().optional().describe("Pull request description (markdown supported)"),
|
|
1011
|
-
reviewerIds: z.array(z.string()).optional().describe("Reviewer GUIDs or unique names"),
|
|
1012
|
-
isDraft: z.boolean().optional().describe("Create as draft PR (default: false)"),
|
|
1013
|
-
}, async ({ project, repositoryId, sourceRefName, targetRefName, title, description, reviewerIds, isDraft }) => {
|
|
1014
|
-
try {
|
|
1015
|
-
const service = getAzureDevOpsService();
|
|
1016
|
-
const result = await service.createPullRequest(project, repositoryId, sourceRefName, targetRefName, title, description, reviewerIds, isDraft);
|
|
1017
|
-
return { content: [{ type: "text", text: `Created PR #${result.pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1018
|
-
}
|
|
1019
|
-
catch (error) {
|
|
1020
|
-
console.error("Error creating pull request:", error);
|
|
1021
|
-
return { content: [{ type: "text", text: `Failed to create pull request: ${error.message}` }] };
|
|
1022
|
-
}
|
|
1023
|
-
});
|
|
1024
|
-
server.tool("update-pull-request", "Update a pull request's title, description, status, or draft state. (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1025
|
-
project: z.string().describe("The project name"),
|
|
1026
|
-
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
1027
|
-
pullRequestId: z.number().describe("The pull request ID"),
|
|
1028
|
-
title: z.string().optional().describe("New title"),
|
|
1029
|
-
description: z.string().optional().describe("New description"),
|
|
1030
|
-
status: z.enum(["abandoned", "active"]).optional().describe("Set PR status (abandoned or active)"),
|
|
1031
|
-
isDraft: z.boolean().optional().describe("Set draft state"),
|
|
1032
|
-
}, async ({ project, repositoryId, pullRequestId, title, description, status, isDraft }) => {
|
|
1033
|
-
try {
|
|
1034
|
-
const service = getAzureDevOpsService();
|
|
1035
|
-
const result = await service.updatePullRequest(project, repositoryId, pullRequestId, { title, description, status, isDraft });
|
|
1036
|
-
return { content: [{ type: "text", text: `Updated PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1037
|
-
}
|
|
1038
|
-
catch (error) {
|
|
1039
|
-
console.error("Error updating pull request:", error);
|
|
1040
|
-
return { content: [{ type: "text", text: `Failed to update pull request: ${error.message}` }] };
|
|
1041
|
-
}
|
|
1042
|
-
});
|
|
1043
|
-
server.tool("complete-pull-request", "Complete (merge) a pull request with configurable merge strategy. (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1044
|
-
project: z.string().describe("The project name"),
|
|
1045
|
-
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
1046
|
-
pullRequestId: z.number().describe("The pull request ID"),
|
|
1047
|
-
mergeStrategy: z.enum(["squash", "noFastForward", "rebase", "rebaseMerge"]).optional()
|
|
1048
|
-
.describe(descWithExamples("Merge strategy (default: squash)", PR_MERGE_STRATEGY_EXAMPLES)),
|
|
1049
|
-
deleteSourceBranch: z.boolean().optional().describe("Delete source branch after merge (default: true)"),
|
|
1050
|
-
transitionWorkItems: z.boolean().optional().describe("Transition linked work items (default: true)"),
|
|
1051
|
-
mergeCommitMessage: z.string().optional().describe("Custom merge commit message"),
|
|
1052
|
-
}, async ({ project, repositoryId, pullRequestId, mergeStrategy, deleteSourceBranch, transitionWorkItems, mergeCommitMessage }) => {
|
|
1053
|
-
try {
|
|
1054
|
-
const service = getAzureDevOpsService();
|
|
1055
|
-
const result = await service.completePullRequest(project, repositoryId, pullRequestId, mergeStrategy || 'squash', deleteSourceBranch !== undefined ? deleteSourceBranch : true, transitionWorkItems !== undefined ? transitionWorkItems : true, mergeCommitMessage);
|
|
1056
|
-
return { content: [{ type: "text", text: `Completed PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1057
|
-
}
|
|
1058
|
-
catch (error) {
|
|
1059
|
-
console.error("Error completing pull request:", error);
|
|
1060
|
-
return { content: [{ type: "text", text: `Failed to complete pull request: ${error.message}` }] };
|
|
1061
|
-
}
|
|
1062
|
-
});
|
|
1063
|
-
server.tool("add-pr-reviewer", "Add or remove a reviewer from a pull request. (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1064
|
-
project: z.string().describe("The project name"),
|
|
1065
|
-
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
1066
|
-
pullRequestId: z.number().describe("The pull request ID"),
|
|
1067
|
-
reviewerId: z.string().describe("Reviewer GUID or unique name"),
|
|
1068
|
-
isRequired: z.boolean().optional().describe("Whether the reviewer is required (default: false)"),
|
|
1069
|
-
remove: z.boolean().optional().describe("Set to true to remove the reviewer instead of adding"),
|
|
1070
|
-
}, async ({ project, repositoryId, pullRequestId, reviewerId, isRequired, remove }) => {
|
|
1071
|
-
try {
|
|
1072
|
-
const service = getAzureDevOpsService();
|
|
1073
|
-
const result = await service.addOrRemovePrReviewer(project, repositoryId, pullRequestId, reviewerId, isRequired, remove);
|
|
1074
|
-
return { content: [{ type: "text", text: `${remove ? 'Removed' : 'Added'} reviewer on PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1075
|
-
}
|
|
1076
|
-
catch (error) {
|
|
1077
|
-
console.error("Error managing PR reviewer:", error);
|
|
1078
|
-
return { content: [{ type: "text", text: `Failed to manage PR reviewer: ${error.message}` }] };
|
|
1079
|
-
}
|
|
1080
|
-
});
|
|
1081
|
-
server.tool("vote-pull-request", "Submit a vote (approve, reject, etc.) on a pull request. Defaults to authenticated user. (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1082
|
-
project: z.string().describe("The project name"),
|
|
1083
|
-
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
1084
|
-
pullRequestId: z.number().describe("The pull request ID"),
|
|
1085
|
-
vote: z.enum(["approve", "approveWithSuggestions", "noResponse", "waitForAuthor", "reject"])
|
|
1086
|
-
.describe(descWithExamples("Vote to submit", PR_VOTE_EXAMPLES)),
|
|
1087
|
-
reviewerId: z.string().optional().describe("Reviewer GUID (defaults to authenticated user)"),
|
|
1088
|
-
}, async ({ project, repositoryId, pullRequestId, vote, reviewerId }) => {
|
|
1089
|
-
try {
|
|
1090
|
-
const service = getAzureDevOpsService();
|
|
1091
|
-
const result = await service.votePullRequest(project, repositoryId, pullRequestId, vote, reviewerId);
|
|
1092
|
-
return { content: [{ type: "text", text: `Voted '${vote}' on PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1093
|
-
}
|
|
1094
|
-
catch (error) {
|
|
1095
|
-
console.error("Error voting on pull request:", error);
|
|
1096
|
-
return { content: [{ type: "text", text: `Failed to vote on pull request: ${error.message}` }] };
|
|
1097
|
-
}
|
|
1098
|
-
});
|
|
1099
|
-
server.tool("reply-to-pr-thread", "Reply to a pull request comment thread and/or update thread status (e.g., resolve). (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1100
|
-
project: z.string().describe("The project name"),
|
|
1101
|
-
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
1102
|
-
pullRequestId: z.number().describe("The pull request ID"),
|
|
1103
|
-
threadId: z.number().describe("The thread ID to reply to"),
|
|
1104
|
-
content: z.string().optional().describe("Reply text (markdown supported)"),
|
|
1105
|
-
status: z.enum(["active", "fixed", "wontFix", "closed", "byDesign", "pending"]).optional()
|
|
1106
|
-
.describe("Update thread status (e.g., 'fixed' to resolve)"),
|
|
1107
|
-
}, async ({ project, repositoryId, pullRequestId, threadId, content, status }) => {
|
|
1108
|
-
try {
|
|
1109
|
-
const service = getAzureDevOpsService();
|
|
1110
|
-
const result = await service.replyToPrThread(project, repositoryId, pullRequestId, threadId, content, status);
|
|
1111
|
-
return { content: [{ type: "text", text: `Reply to thread #${threadId} on PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1112
|
-
}
|
|
1113
|
-
catch (error) {
|
|
1114
|
-
console.error("Error replying to PR thread:", error);
|
|
1115
|
-
return { content: [{ type: "text", text: `Failed to reply to PR thread: ${error.message}` }] };
|
|
1116
|
-
}
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
// ========================================
|
|
1120
|
-
// VARIABLE GROUP TOOLS
|
|
1121
|
-
// ========================================
|
|
1122
|
-
server.tool("list-variable-groups", "List all variable groups in an Azure DevOps project. Variable groups store values and secrets used in pipelines.", {
|
|
1123
|
-
project: z.string().describe("The project name"),
|
|
1124
|
-
}, async ({ project }) => {
|
|
1125
|
-
try {
|
|
1126
|
-
const service = getAzureDevOpsService();
|
|
1127
|
-
const result = await service.getVariableGroups(project);
|
|
1128
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
1129
|
-
return {
|
|
1130
|
-
content: [
|
|
1131
|
-
{
|
|
1132
|
-
type: "text",
|
|
1133
|
-
text: `Variable groups in project '${project}':\n\n${resultStr}`,
|
|
1134
|
-
},
|
|
1135
|
-
],
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1138
|
-
catch (error) {
|
|
1139
|
-
console.error("Error listing variable groups:", error);
|
|
1140
|
-
return {
|
|
1141
|
-
content: [
|
|
1142
|
-
{
|
|
1143
|
-
type: "text",
|
|
1144
|
-
text: `Failed to list variable groups: ${error.message}`,
|
|
1145
|
-
},
|
|
1146
|
-
],
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
});
|
|
1150
|
-
server.tool("get-variable-group", "Get a specific variable group by ID from Azure DevOps. Returns all variables (secrets are masked).", {
|
|
1151
|
-
project: z.string().describe("The project name"),
|
|
1152
|
-
groupId: z.number().describe("The variable group ID"),
|
|
1153
|
-
}, async ({ project, groupId }) => {
|
|
1154
|
-
try {
|
|
1155
|
-
const service = getAzureDevOpsService();
|
|
1156
|
-
const result = await service.getVariableGroup(project, groupId);
|
|
1157
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
1158
|
-
return {
|
|
1159
|
-
content: [
|
|
1160
|
-
{
|
|
1161
|
-
type: "text",
|
|
1162
|
-
text: `Variable group ${groupId} in project '${project}':\n\n${resultStr}`,
|
|
1163
|
-
},
|
|
1164
|
-
],
|
|
1165
|
-
};
|
|
1166
|
-
}
|
|
1167
|
-
catch (error) {
|
|
1168
|
-
console.error("Error getting variable group:", error);
|
|
1169
|
-
return {
|
|
1170
|
-
content: [
|
|
1171
|
-
{
|
|
1172
|
-
type: "text",
|
|
1173
|
-
text: `Failed to get variable group: ${error.message}`,
|
|
1174
|
-
},
|
|
1175
|
-
],
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
});
|
|
1179
|
-
// ========================================
|
|
1180
|
-
// BUILD TROUBLESHOOTING TOOLS (Read-only)
|
|
1181
|
-
// NOTE: These tools are duplicated in azure-devops-admin package.
|
|
1182
|
-
// If you update these, also update packages/azure-devops-admin/src/index.ts
|
|
1183
|
-
// ========================================
|
|
1184
|
-
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).", {
|
|
1185
|
-
project: z.string().describe("The project name"),
|
|
1186
|
-
buildId: z.number().describe("The build ID"),
|
|
1187
|
-
detail: z.enum(["summary", "timeline", "full"]).optional().describe("Level of detail: 'summary' (default), 'timeline' (include steps), or 'full' (include logs)"),
|
|
1188
|
-
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)"),
|
|
1189
|
-
maxIssues: z.number().optional().describe("Maximum issues per record (default: 5, prioritizes errors over warnings)"),
|
|
1190
|
-
}, async ({ project, buildId, detail, timelineScope, maxIssues }) => {
|
|
1191
|
-
try {
|
|
1192
|
-
const service = getAzureDevOpsService();
|
|
1193
|
-
const result = await service.getBuildStatus(project, buildId, detail || 'summary', timelineScope || 'problems', maxIssues || 5);
|
|
1194
|
-
return { content: [{ type: "text", text: `Build ${buildId} status:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1195
|
-
}
|
|
1196
|
-
catch (error) {
|
|
1197
|
-
console.error("Error getting build status:", error);
|
|
1198
|
-
return { content: [{ type: "text", text: `Failed to get build status: ${error.message}` }] };
|
|
1199
|
-
}
|
|
1200
|
-
});
|
|
1201
|
-
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.", {
|
|
1202
|
-
project: z.string().describe("The project name"),
|
|
1203
|
-
buildId: z.number().describe("The build ID"),
|
|
1204
|
-
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)"),
|
|
1205
|
-
maxIssues: z.number().optional().describe("Maximum issues per record (default: 5, prioritizes errors over warnings)"),
|
|
1206
|
-
}, async ({ project, buildId, scope, maxIssues }) => {
|
|
1207
|
-
try {
|
|
1208
|
-
const service = getAzureDevOpsService();
|
|
1209
|
-
const result = await service.getBuildTimeline(project, buildId, scope || 'problems', maxIssues || 5);
|
|
1210
|
-
return { content: [{ type: "text", text: `Build ${buildId} timeline:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1211
|
-
}
|
|
1212
|
-
catch (error) {
|
|
1213
|
-
console.error("Error getting build timeline:", error);
|
|
1214
|
-
return { content: [{ type: "text", text: `Failed to get build timeline: ${error.message}` }] };
|
|
1215
|
-
}
|
|
1216
|
-
});
|
|
1217
|
-
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 filtered by mode to reduce noise from progress indicators.", {
|
|
1218
|
-
project: z.string().describe("The project name"),
|
|
1219
|
-
buildId: z.number().describe("The build ID"),
|
|
1220
|
-
logId: z.number().optional().describe("Optional specific log ID to retrieve content"),
|
|
1221
|
-
mode: z.enum(['summary', 'full', 'errors']).optional().describe("Filter mode: 'summary' (default) removes progress indicators like 'Receiving objects: 1%', 'full' returns everything, 'errors' shows only errors/warnings"),
|
|
1222
|
-
}, async ({ project, buildId, logId, mode }) => {
|
|
1223
|
-
try {
|
|
1224
|
-
const service = getAzureDevOpsService();
|
|
1225
|
-
const result = await service.getBuildLogs(project, buildId, logId, mode || 'summary');
|
|
1226
|
-
return { content: [{ type: "text", text: `Build ${buildId} logs:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1227
|
-
}
|
|
1228
|
-
catch (error) {
|
|
1229
|
-
console.error("Error getting build logs:", error);
|
|
1230
|
-
return { content: [{ type: "text", text: `Failed to get build logs: ${error.message}` }] };
|
|
1231
|
-
}
|
|
1232
|
-
});
|
|
1233
|
-
// ========================================
|
|
1234
|
-
// WORK ITEM SYNC TOOLS (Local Markdown)
|
|
1235
|
-
// ========================================
|
|
1236
|
-
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), {
|
|
1237
|
-
project: z.string().describe("The project name"),
|
|
1238
|
-
workItemIds: z.array(z.number()).default([]).describe("Work item IDs to pull (optional if using parentId)"),
|
|
1239
|
-
parentId: z.number().optional().describe("Pull all child work items of this parent (e.g., Feature ID to pull all User Stories)"),
|
|
1240
|
-
childType: z.string().optional().describe("Filter by work item type when using parentId (default: 'User Story'). Common values: 'User Story', 'Bug', 'Task'"),
|
|
1241
|
-
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1242
|
-
includeComments: z.boolean().optional().describe("Also save comments to {id}-comments.md (default: false)"),
|
|
1243
|
-
skipAutoConvert: z.boolean().optional().describe("Skip automatic HTML-to-markdown conversion. Only use when explicitly requested. Default: false (auto-convert enabled)"),
|
|
1244
|
-
}, async ({ project, workItemIds: providedWorkItemIds, parentId, childType, folder, includeComments, skipAutoConvert }) => {
|
|
1245
|
-
try {
|
|
1246
|
-
const service = getAzureDevOpsService();
|
|
1247
|
-
const syncConfig = getSyncConfig(folder);
|
|
1248
|
-
// Validate folder path for security
|
|
1249
|
-
validateFolderPath(syncConfig.folder);
|
|
1250
|
-
// Determine which work items to pull
|
|
1251
|
-
let workItemIds = providedWorkItemIds || [];
|
|
1252
|
-
// If parentId is provided, query for child work items
|
|
1253
|
-
if (parentId) {
|
|
1254
|
-
const type = childType || 'User Story';
|
|
1255
|
-
const wiql = `SELECT [System.Id] FROM WorkItems WHERE [System.Parent] = ${parentId} AND [System.WorkItemType] = '${type}' ORDER BY [System.Id] ASC`;
|
|
1256
|
-
const queryResult = await service.queryWorkItems(project, wiql, 200);
|
|
1257
|
-
if (queryResult.workItems && queryResult.workItems.length > 0) {
|
|
1258
|
-
const childIds = queryResult.workItems.map((wi) => wi.id);
|
|
1259
|
-
workItemIds = [...workItemIds, ...childIds];
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
// Validate that we have work items to pull
|
|
1263
|
-
if (workItemIds.length === 0) {
|
|
1264
|
-
return {
|
|
1265
|
-
content: [{
|
|
1266
|
-
type: "text",
|
|
1267
|
-
text: parentId
|
|
1268
|
-
? `No ${childType || 'User Story'} work items found under parent #${parentId}`
|
|
1269
|
-
: "No work item IDs provided. Specify workItemIds or parentId.",
|
|
1270
|
-
}],
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
|
-
// Remove duplicates
|
|
1274
|
-
workItemIds = [...new Set(workItemIds)];
|
|
1275
|
-
const pulled = [];
|
|
1276
|
-
const skipped = [];
|
|
1277
|
-
const commentsFiles = [];
|
|
1278
|
-
const filesToCommit = [];
|
|
1279
|
-
await ensureFolderExists(syncConfig.folder);
|
|
1280
|
-
for (const workItemId of workItemIds) {
|
|
1281
|
-
try {
|
|
1282
|
-
// Fetch work item from ADO
|
|
1283
|
-
let workItem = await service.getWorkItem(project, workItemId);
|
|
1284
|
-
let revision = workItem.rev || workItem._rev || 1;
|
|
1285
|
-
let convertedFields = [];
|
|
1286
|
-
// Check field formats
|
|
1287
|
-
const formats = checkFieldFormats(workItem);
|
|
1288
|
-
if (!formats.ready) {
|
|
1289
|
-
if (skipAutoConvert) {
|
|
1290
|
-
// User explicitly requested to skip conversion
|
|
1291
|
-
const htmlFields = [];
|
|
1292
|
-
if (formats.description === 'html')
|
|
1293
|
-
htmlFields.push('Description');
|
|
1294
|
-
if (formats.acceptanceCriteria === 'html')
|
|
1295
|
-
htmlFields.push('Acceptance Criteria');
|
|
1296
|
-
skipped.push({
|
|
1297
|
-
id: workItemId,
|
|
1298
|
-
reason: `HTML fields: ${htmlFields.join(', ')}. skipAutoConvert=true, skipping.`,
|
|
1299
|
-
});
|
|
1300
|
-
continue;
|
|
1301
|
-
}
|
|
1302
|
-
// Auto-convert HTML fields to markdown in ADO
|
|
1303
|
-
const fieldsToConvert = [];
|
|
1304
|
-
if (formats.description === 'html')
|
|
1305
|
-
fieldsToConvert.push('System.Description');
|
|
1306
|
-
if (formats.acceptanceCriteria === 'html')
|
|
1307
|
-
fieldsToConvert.push('Microsoft.VSTS.Common.AcceptanceCriteria');
|
|
1308
|
-
try {
|
|
1309
|
-
convertedFields = await autoConvertFieldsToMarkdown(service, project, workItemId, workItem.fields, fieldsToConvert);
|
|
1310
|
-
// Re-fetch work item with converted content
|
|
1311
|
-
workItem = await service.getWorkItem(project, workItemId);
|
|
1312
|
-
revision = workItem.rev || workItem._rev || 1;
|
|
1313
|
-
}
|
|
1314
|
-
catch (convertError) {
|
|
1315
|
-
skipped.push({
|
|
1316
|
-
id: workItemId,
|
|
1317
|
-
reason: `Failed to auto-convert HTML: ${convertError.message}`,
|
|
1318
|
-
});
|
|
1319
|
-
continue;
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
// Convert to markdown and save
|
|
1323
|
-
const { content: markdown, skippedFields: secondarySkipped } = workItemToMarkdown(workItem, revision);
|
|
1324
|
-
const filePath = getWorkItemFilePath(syncConfig.folder, workItemId);
|
|
1325
|
-
await writeWorkItemFile(filePath, markdown);
|
|
1326
|
-
pulled.push({
|
|
1327
|
-
id: workItemId,
|
|
1328
|
-
file: filePath,
|
|
1329
|
-
revision,
|
|
1330
|
-
...(convertedFields.length > 0 ? { converted: convertedFields } : {}),
|
|
1331
|
-
...(secondarySkipped.length > 0 ? { skippedFields: secondarySkipped } : {}),
|
|
1332
|
-
});
|
|
1333
|
-
filesToCommit.push({ filePath, workItemId });
|
|
1334
|
-
// Optionally save comments
|
|
1335
|
-
if (includeComments) {
|
|
1336
|
-
const comments = await service.getWorkItemComments(project, workItemId);
|
|
1337
|
-
const commentsMarkdown = commentsToMarkdown(workItem, comments.comments || []);
|
|
1338
|
-
const commentsPath = getCommentsFilePath(syncConfig.folder, workItemId);
|
|
1339
|
-
await writeWorkItemFile(commentsPath, commentsMarkdown);
|
|
1340
|
-
commentsFiles.push({
|
|
1341
|
-
id: workItemId,
|
|
1342
|
-
file: commentsPath,
|
|
1343
|
-
count: (comments.comments || []).length,
|
|
1344
|
-
});
|
|
1345
|
-
filesToCommit.push({ filePath: commentsPath, workItemId });
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
catch (error) {
|
|
1349
|
-
skipped.push({ id: workItemId, reason: error.message });
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
// Auto-commit if enabled
|
|
1353
|
-
let committed = false;
|
|
1354
|
-
if (syncConfig.autoCommit && filesToCommit.length > 0) {
|
|
1355
|
-
const commitResult = await autoCommitMultipleFiles(filesToCommit, 'synced');
|
|
1356
|
-
committed = commitResult.committed;
|
|
1357
|
-
}
|
|
1358
|
-
const result = {
|
|
1359
|
-
pulled,
|
|
1360
|
-
skipped,
|
|
1361
|
-
...(includeComments ? { commentsFiles } : {}),
|
|
1362
|
-
folder: syncConfig.folder,
|
|
1363
|
-
committed,
|
|
1364
|
-
};
|
|
1365
|
-
return {
|
|
1366
|
-
content: [{
|
|
1367
|
-
type: "text",
|
|
1368
|
-
text: `Synced ${pulled.length} work item(s) to local files:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1369
|
-
}],
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
catch (error) {
|
|
1373
|
-
console.error("Error syncing work items to files:", error);
|
|
1374
|
-
return {
|
|
1375
|
-
content: [{
|
|
1376
|
-
type: "text",
|
|
1377
|
-
text: `Failed to sync work items: ${error.message}`,
|
|
1378
|
-
}],
|
|
1379
|
-
};
|
|
1380
|
-
}
|
|
1381
|
-
});
|
|
1382
|
-
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), {
|
|
1383
|
-
project: z.string().describe("The project name"),
|
|
1384
|
-
workItemIds: z.array(z.number()).default([]).describe("Work item IDs to push (optional - new_*.md files are auto-detected)"),
|
|
1385
|
-
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1386
|
-
skipAutoConvert: z.boolean().optional().describe("Skip automatic HTML-to-markdown conversion. Only use when explicitly requested. Default: false (auto-convert enabled)"),
|
|
1387
|
-
}, async ({ project, workItemIds, folder, skipAutoConvert }) => {
|
|
1388
|
-
try {
|
|
1389
|
-
const service = getAzureDevOpsService();
|
|
1390
|
-
const syncConfig = getSyncConfig(folder);
|
|
1391
|
-
// Validate folder path for security
|
|
1392
|
-
validateFolderPath(syncConfig.folder);
|
|
1393
|
-
const pushed = [];
|
|
1394
|
-
const partial = [];
|
|
1395
|
-
const created = [];
|
|
1396
|
-
const failed = [];
|
|
1397
|
-
// Step 1: Auto-detect and process new_*.md files
|
|
1398
|
-
const newFiles = await findNewWorkItemFiles(syncConfig.folder);
|
|
1399
|
-
for (const filePath of newFiles) {
|
|
1400
|
-
try {
|
|
1401
|
-
// Parse the new work item file
|
|
1402
|
-
const content = await readFileContent(filePath);
|
|
1403
|
-
const parsed = parseNewWorkItemMarkdown(content);
|
|
1404
|
-
// Fetch parent work item to inherit area/iteration paths
|
|
1405
|
-
const parentWorkItem = await service.getWorkItem(project, parsed.frontmatter.parent);
|
|
1406
|
-
// Build fields for creation
|
|
1407
|
-
const fields = buildNewWorkItemFields(parsed, parentWorkItem);
|
|
1408
|
-
// Create the work item in ADO with parent link
|
|
1409
|
-
const createdWorkItem = await service.createWorkItem(project, parsed.frontmatter.type || 'User Story', fields, parsed.frontmatter.parent);
|
|
1410
|
-
const newId = createdWorkItem.id;
|
|
1411
|
-
const revision = createdWorkItem.rev || createdWorkItem._rev || 1;
|
|
1412
|
-
const url = createdWorkItem._links?.html?.href || `https://dev.azure.com/_workitems/edit/${newId}`;
|
|
1413
|
-
// Convert the file content to synced format
|
|
1414
|
-
const syncedContent = convertNewFileToSynced(content, newId, revision, url);
|
|
1415
|
-
// Rename file from new_*.md to {id}.md
|
|
1416
|
-
const newFilePath = getWorkItemFilePath(syncConfig.folder, newId);
|
|
1417
|
-
await writeWorkItemFile(newFilePath, syncedContent);
|
|
1418
|
-
await renameFile(filePath, filePath + '.created'); // Mark original as processed
|
|
1419
|
-
// Actually delete the .created file (cleanup)
|
|
1420
|
-
try {
|
|
1421
|
-
const fs = await import('node:fs/promises');
|
|
1422
|
-
await fs.unlink(filePath + '.created');
|
|
1423
|
-
}
|
|
1424
|
-
catch {
|
|
1425
|
-
// Ignore cleanup errors
|
|
1426
|
-
}
|
|
1427
|
-
created.push({
|
|
1428
|
-
id: newId,
|
|
1429
|
-
oldFile: filePath,
|
|
1430
|
-
newFile: newFilePath,
|
|
1431
|
-
parentId: parsed.frontmatter.parent,
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
catch (error) {
|
|
1435
|
-
failed.push({ file: filePath, error: error.message });
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
// Step 2: Process existing work items (if workItemIds provided)
|
|
1439
|
-
const idsToProcess = workItemIds || [];
|
|
1440
|
-
for (const workItemId of idsToProcess) {
|
|
1441
|
-
const filePath = getWorkItemFilePath(syncConfig.folder, workItemId);
|
|
1442
|
-
try {
|
|
1443
|
-
// Check if file exists
|
|
1444
|
-
if (!await fileExists(filePath)) {
|
|
1445
|
-
failed.push({ id: workItemId, error: `File not found: ${filePath}` });
|
|
1446
|
-
continue;
|
|
1447
|
-
}
|
|
1448
|
-
// Parse local file
|
|
1449
|
-
const parsed = await readWorkItemFile(filePath);
|
|
1450
|
-
const oldRevision = parsed.frontmatter.lastSyncedRevision;
|
|
1451
|
-
// Fetch current work item from ADO to compare
|
|
1452
|
-
const currentWorkItem = await service.getWorkItem(project, workItemId);
|
|
1453
|
-
// Build patch operations (auto-converts HTML fields unless skipAutoConvert)
|
|
1454
|
-
const { operations, skippedFields, convertedFields } = buildPatchOperations(parsed, currentWorkItem, skipAutoConvert);
|
|
1455
|
-
if (operations.length === 0 && skippedFields.length === 0) {
|
|
1456
|
-
// No changes
|
|
1457
|
-
pushed.push({
|
|
1458
|
-
id: workItemId,
|
|
1459
|
-
oldRevision,
|
|
1460
|
-
newRevision: currentWorkItem.rev || currentWorkItem._rev || oldRevision,
|
|
1461
|
-
fieldsUpdated: [],
|
|
1462
|
-
});
|
|
1463
|
-
continue;
|
|
1464
|
-
}
|
|
1465
|
-
// Update ADO if there are operations
|
|
1466
|
-
let newRevision = oldRevision;
|
|
1467
|
-
let fieldsUpdated = [];
|
|
1468
|
-
if (operations.length > 0) {
|
|
1469
|
-
const updatedWorkItem = await service.updateWorkItem(project, workItemId, operations);
|
|
1470
|
-
newRevision = updatedWorkItem.rev || updatedWorkItem._rev || oldRevision + 1;
|
|
1471
|
-
// Filter out format operations from fieldsUpdated
|
|
1472
|
-
fieldsUpdated = operations
|
|
1473
|
-
.filter(op => op.path.startsWith('/fields/'))
|
|
1474
|
-
.map(op => op.path.replace('/fields/', ''));
|
|
1475
|
-
// Update local file with new revision
|
|
1476
|
-
const content = await readFileContent(filePath);
|
|
1477
|
-
const updatedContent = updateSyncRevision(content, newRevision);
|
|
1478
|
-
await writeWorkItemFile(filePath, updatedContent);
|
|
1479
|
-
}
|
|
1480
|
-
if (skippedFields.length > 0) {
|
|
1481
|
-
partial.push({
|
|
1482
|
-
id: workItemId,
|
|
1483
|
-
oldRevision,
|
|
1484
|
-
newRevision,
|
|
1485
|
-
fieldsUpdated,
|
|
1486
|
-
skippedFields,
|
|
1487
|
-
...(convertedFields.length > 0 ? { convertedFields } : {}),
|
|
1488
|
-
});
|
|
1489
|
-
}
|
|
1490
|
-
else {
|
|
1491
|
-
pushed.push({
|
|
1492
|
-
id: workItemId,
|
|
1493
|
-
oldRevision,
|
|
1494
|
-
newRevision,
|
|
1495
|
-
fieldsUpdated,
|
|
1496
|
-
...(convertedFields.length > 0 ? { convertedFields } : {}),
|
|
1497
|
-
});
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
catch (error) {
|
|
1501
|
-
failed.push({ id: workItemId, error: error.message });
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
const result = {
|
|
1505
|
-
created,
|
|
1506
|
-
pushed,
|
|
1507
|
-
partial,
|
|
1508
|
-
failed,
|
|
1509
|
-
folder: syncConfig.folder,
|
|
1510
|
-
};
|
|
1511
|
-
const summary = [];
|
|
1512
|
-
if (created.length > 0)
|
|
1513
|
-
summary.push(`Created ${created.length} new work item(s)`);
|
|
1514
|
-
if (pushed.length > 0)
|
|
1515
|
-
summary.push(`Updated ${pushed.length} work item(s)`);
|
|
1516
|
-
if (partial.length > 0)
|
|
1517
|
-
summary.push(`Partially updated ${partial.length} work item(s)`);
|
|
1518
|
-
if (failed.length > 0)
|
|
1519
|
-
summary.push(`Failed: ${failed.length}`);
|
|
1520
|
-
return {
|
|
1521
|
-
content: [{
|
|
1522
|
-
type: "text",
|
|
1523
|
-
text: `${summary.join(', ')}:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1524
|
-
}],
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
catch (error) {
|
|
1528
|
-
console.error("Error syncing work items from files:", error);
|
|
1529
|
-
return {
|
|
1530
|
-
content: [{
|
|
1531
|
-
type: "text",
|
|
1532
|
-
text: `Failed to push work items: ${error.message}`,
|
|
1533
|
-
}],
|
|
1534
|
-
};
|
|
1535
|
-
}
|
|
1536
|
-
});
|
|
1537
|
-
server.tool("check-work-item-markdown", "Check if work item fields are markdown (required for sync) or HTML format.", {
|
|
1538
|
-
project: z.string().describe("The project name"),
|
|
1539
|
-
workItemIds: z.array(z.number()).describe("Work item IDs to check"),
|
|
1540
|
-
}, async ({ project, workItemIds }) => {
|
|
1541
|
-
try {
|
|
1542
|
-
const service = getAzureDevOpsService();
|
|
1543
|
-
const results = [];
|
|
1544
|
-
let readyCount = 0;
|
|
1545
|
-
let needsConversionCount = 0;
|
|
1546
|
-
for (const workItemId of workItemIds) {
|
|
1547
|
-
try {
|
|
1548
|
-
const workItem = await service.getWorkItem(project, workItemId);
|
|
1549
|
-
const formats = checkFieldFormats(workItem);
|
|
1550
|
-
results.push({
|
|
1551
|
-
id: workItemId,
|
|
1552
|
-
description: formats.description,
|
|
1553
|
-
acceptanceCriteria: formats.acceptanceCriteria,
|
|
1554
|
-
howToTest: formats.additionalFields.howToTest,
|
|
1555
|
-
predeploymentSteps: formats.additionalFields.predeploymentSteps,
|
|
1556
|
-
postdeploymentSteps: formats.additionalFields.postdeploymentSteps,
|
|
1557
|
-
deploymentInformation: formats.additionalFields.deploymentInformation,
|
|
1558
|
-
ready: formats.ready,
|
|
1559
|
-
...(formats.warnings.length > 0 ? { warnings: formats.warnings } : {}),
|
|
1560
|
-
});
|
|
1561
|
-
if (formats.ready) {
|
|
1562
|
-
readyCount++;
|
|
1563
|
-
}
|
|
1564
|
-
else {
|
|
1565
|
-
needsConversionCount++;
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
catch (error) {
|
|
1569
|
-
results.push({
|
|
1570
|
-
id: workItemId,
|
|
1571
|
-
description: 'error',
|
|
1572
|
-
acceptanceCriteria: 'error',
|
|
1573
|
-
ready: false,
|
|
1574
|
-
error: error.message || String(error),
|
|
1575
|
-
});
|
|
1576
|
-
needsConversionCount++;
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
const result = {
|
|
1580
|
-
results,
|
|
1581
|
-
summary: {
|
|
1582
|
-
ready: readyCount,
|
|
1583
|
-
needsConversion: needsConversionCount,
|
|
1584
|
-
},
|
|
1585
|
-
autoConvertAvailable: true,
|
|
1586
|
-
message: needsConversionCount === 0
|
|
1587
|
-
? 'All work items are markdown format - ready to sync'
|
|
1588
|
-
: `${needsConversionCount} work item(s) have HTML fields. Will be auto-converted to markdown on sync (unless skipAutoConvert=true).`,
|
|
1589
|
-
};
|
|
1590
|
-
return {
|
|
1591
|
-
content: [{
|
|
1592
|
-
type: "text",
|
|
1593
|
-
text: `Work item format check:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1594
|
-
}],
|
|
1595
|
-
};
|
|
1596
|
-
}
|
|
1597
|
-
catch (error) {
|
|
1598
|
-
console.error("Error checking work item formats:", error);
|
|
1599
|
-
return {
|
|
1600
|
-
content: [{
|
|
1601
|
-
type: "text",
|
|
1602
|
-
text: `Failed to check work item formats: ${error.message}`,
|
|
1603
|
-
}],
|
|
1604
|
-
};
|
|
1605
|
-
}
|
|
1606
|
-
});
|
|
1607
|
-
server.tool("list-synced-work-items", "List work items that have been synced to local markdown files.", {
|
|
1608
|
-
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1609
|
-
}, async ({ folder }) => {
|
|
1610
|
-
try {
|
|
1611
|
-
const syncConfig = getSyncConfig(folder);
|
|
1612
|
-
// Validate folder path for security
|
|
1613
|
-
validateFolderPath(syncConfig.folder);
|
|
1614
|
-
const workItems = await listSyncedWorkItems(syncConfig.folder);
|
|
1615
|
-
const result = {
|
|
1616
|
-
workItems,
|
|
1617
|
-
folder: syncConfig.folder,
|
|
1618
|
-
count: workItems.length,
|
|
1619
|
-
};
|
|
1620
|
-
return {
|
|
1621
|
-
content: [{
|
|
1622
|
-
type: "text",
|
|
1623
|
-
text: `Synced work items in ${syncConfig.folder}:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1624
|
-
}],
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
1627
|
-
catch (error) {
|
|
1628
|
-
console.error("Error listing synced work items:", error);
|
|
1629
|
-
return {
|
|
1630
|
-
content: [{
|
|
1631
|
-
type: "text",
|
|
1632
|
-
text: `Failed to list synced work items: ${error.message}`,
|
|
1633
|
-
}],
|
|
1634
|
-
};
|
|
1635
|
-
}
|
|
1636
|
-
});
|
|
1637
|
-
server.tool("create-user-story-file", "Create a new user story template file locally. The file can be edited and then pushed to ADO using sync-work-item-from-file.", {
|
|
1638
|
-
project: z.string().describe("The project name"),
|
|
1639
|
-
parentId: z.number().describe("Parent Feature ID - the new user story will be created under this feature"),
|
|
1640
|
-
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1641
|
-
}, async ({ project, parentId, folder }) => {
|
|
1642
|
-
try {
|
|
1643
|
-
const service = getAzureDevOpsService();
|
|
1644
|
-
const syncConfig = getSyncConfig(folder);
|
|
1645
|
-
// Validate folder path for security
|
|
1646
|
-
validateFolderPath(syncConfig.folder);
|
|
1647
|
-
// Fetch parent work item to get title
|
|
1648
|
-
const parentWorkItem = await service.getWorkItem(project, parentId);
|
|
1649
|
-
const parentTitle = parentWorkItem.fields?.['System.Title'] || '';
|
|
1650
|
-
// Find next available index for new file
|
|
1651
|
-
const nextIndex = await findNextNewFileIndex(syncConfig.folder, parentId);
|
|
1652
|
-
const filePath = getNewWorkItemFilePath(syncConfig.folder, parentId, nextIndex);
|
|
1653
|
-
// Generate template content
|
|
1654
|
-
const template = generateNewWorkItemTemplate(parentId, parentTitle, project, 'User Story');
|
|
1655
|
-
// Ensure folder exists and write file
|
|
1656
|
-
await ensureFolderExists(syncConfig.folder);
|
|
1657
|
-
await writeWorkItemFile(filePath, template);
|
|
1658
|
-
const result = {
|
|
1659
|
-
file: filePath,
|
|
1660
|
-
parentId,
|
|
1661
|
-
parentTitle,
|
|
1662
|
-
instructions: [
|
|
1663
|
-
`1. Edit the file to update title, description, and acceptance criteria`,
|
|
1664
|
-
`2. Run sync-work-item-from-file(project: "${project}") to create in ADO`,
|
|
1665
|
-
`3. The file will be renamed to {newId}.md after creation`,
|
|
1666
|
-
],
|
|
1667
|
-
};
|
|
1668
|
-
return {
|
|
1669
|
-
content: [{
|
|
1670
|
-
type: "text",
|
|
1671
|
-
text: `Created new user story template:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1672
|
-
}],
|
|
1673
|
-
};
|
|
1674
|
-
}
|
|
1675
|
-
catch (error) {
|
|
1676
|
-
console.error("Error creating user story file:", error);
|
|
1677
|
-
return {
|
|
1678
|
-
content: [{
|
|
1679
|
-
type: "text",
|
|
1680
|
-
text: `Failed to create user story file: ${error.message}`,
|
|
1681
|
-
}],
|
|
1682
|
-
};
|
|
1683
|
-
}
|
|
1684
|
-
});
|
|
1685
|
-
// ========================================
|
|
1686
|
-
// TASK SYNC TOOLS
|
|
1687
|
-
// ========================================
|
|
1688
|
-
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), {
|
|
1689
|
-
project: z.string().describe("The project name"),
|
|
1690
|
-
parentIds: z.array(z.number()).describe("Parent work item IDs (User Stories) to fetch tasks for"),
|
|
1691
|
-
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1692
|
-
skipAutoConvert: z.boolean().optional().describe("Skip automatic HTML-to-markdown conversion for task descriptions. Only use when explicitly requested. Default: false"),
|
|
1693
|
-
}, async ({ project, parentIds, folder, skipAutoConvert }) => {
|
|
1694
|
-
try {
|
|
1695
|
-
const service = getAzureDevOpsService();
|
|
1696
|
-
const syncConfig = getSyncConfig(folder);
|
|
1697
|
-
// Validate folder path for security
|
|
1698
|
-
validateFolderPath(syncConfig.folder);
|
|
1699
|
-
const pulled = [];
|
|
1700
|
-
const failed = [];
|
|
1701
|
-
const filesToCommit = [];
|
|
1702
|
-
await ensureFolderExists(syncConfig.folder);
|
|
1703
|
-
for (const parentId of parentIds) {
|
|
1704
|
-
try {
|
|
1705
|
-
// Fetch parent work item to get title and validate existence
|
|
1706
|
-
const parentWorkItem = await service.getWorkItem(project, parentId);
|
|
1707
|
-
// Query for child tasks using WIQL
|
|
1708
|
-
const wiql = `SELECT [System.Id] FROM WorkItems WHERE [System.Parent] = ${parentId} AND [System.WorkItemType] = 'Task' ORDER BY [System.Id] ASC`;
|
|
1709
|
-
const queryResult = await service.queryWorkItems(project, wiql, 200);
|
|
1710
|
-
// Fetch full details for each task and auto-convert HTML descriptions
|
|
1711
|
-
const tasks = [];
|
|
1712
|
-
let tasksConverted = 0;
|
|
1713
|
-
if (queryResult.workItems && queryResult.workItems.length > 0) {
|
|
1714
|
-
for (const wi of queryResult.workItems) {
|
|
1715
|
-
try {
|
|
1716
|
-
let task = await service.getWorkItem(project, wi.id);
|
|
1717
|
-
// Auto-convert HTML description to markdown unless skipAutoConvert
|
|
1718
|
-
const description = task.fields?.['System.Description'];
|
|
1719
|
-
if (description && !skipAutoConvert && isHtmlContent(description)) {
|
|
1720
|
-
try {
|
|
1721
|
-
await autoConvertFieldsToMarkdown(service, project, wi.id, task.fields, ['System.Description']);
|
|
1722
|
-
// Re-fetch with converted content
|
|
1723
|
-
task = await service.getWorkItem(project, wi.id);
|
|
1724
|
-
tasksConverted++;
|
|
1725
|
-
}
|
|
1726
|
-
catch (convertError) {
|
|
1727
|
-
console.error(`Failed to convert HTML description for task ${wi.id}:`, convertError.message);
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
tasks.push(task);
|
|
1731
|
-
}
|
|
1732
|
-
catch (taskError) {
|
|
1733
|
-
console.error(`Error fetching task ${wi.id}:`, taskError.message);
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
// Convert to markdown and save
|
|
1738
|
-
const markdown = tasksToMarkdown(parentWorkItem, tasks, project);
|
|
1739
|
-
const filePath = getTasksFilePath(syncConfig.folder, parentId);
|
|
1740
|
-
await writeWorkItemFile(filePath, markdown);
|
|
1741
|
-
pulled.push({
|
|
1742
|
-
parentId,
|
|
1743
|
-
file: filePath,
|
|
1744
|
-
taskCount: tasks.length,
|
|
1745
|
-
});
|
|
1746
|
-
filesToCommit.push({ filePath, workItemId: parentId });
|
|
1747
|
-
}
|
|
1748
|
-
catch (error) {
|
|
1749
|
-
failed.push({ parentId, error: error.message });
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
// Auto-commit if enabled
|
|
1753
|
-
let committed = false;
|
|
1754
|
-
if (syncConfig.autoCommit && filesToCommit.length > 0) {
|
|
1755
|
-
const commitResult = await autoCommitMultipleFiles(filesToCommit, 'synced tasks for');
|
|
1756
|
-
committed = commitResult.committed;
|
|
1757
|
-
}
|
|
1758
|
-
const result = {
|
|
1759
|
-
pulled,
|
|
1760
|
-
failed,
|
|
1761
|
-
folder: syncConfig.folder,
|
|
1762
|
-
committed,
|
|
1763
|
-
};
|
|
1764
|
-
return {
|
|
1765
|
-
content: [{
|
|
1766
|
-
type: "text",
|
|
1767
|
-
text: `Synced tasks for ${pulled.length} parent(s) to local files:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1768
|
-
}],
|
|
1769
|
-
};
|
|
1770
|
-
}
|
|
1771
|
-
catch (error) {
|
|
1772
|
-
console.error("Error syncing tasks to files:", error);
|
|
1773
|
-
return {
|
|
1774
|
-
content: [{
|
|
1775
|
-
type: "text",
|
|
1776
|
-
text: `Failed to sync tasks: ${error.message}`,
|
|
1777
|
-
}],
|
|
1778
|
-
};
|
|
1779
|
-
}
|
|
1780
|
-
});
|
|
1781
|
-
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), {
|
|
1782
|
-
project: z.string().describe("The project name"),
|
|
1783
|
-
parentIds: z.array(z.number()).describe("Parent work item IDs to sync tasks for"),
|
|
1784
|
-
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1785
|
-
skipAutoConvert: z.boolean().optional().describe("Skip automatic HTML-to-markdown conversion for task descriptions. Only use when explicitly requested. Default: false"),
|
|
1786
|
-
}, async ({ project, parentIds, folder, skipAutoConvert }) => {
|
|
1787
|
-
try {
|
|
1788
|
-
const service = getAzureDevOpsService();
|
|
1789
|
-
const syncConfig = getSyncConfig(folder);
|
|
1790
|
-
// Validate folder path for security
|
|
1791
|
-
validateFolderPath(syncConfig.folder);
|
|
1792
|
-
const updated = [];
|
|
1793
|
-
const created = [];
|
|
1794
|
-
const failed = [];
|
|
1795
|
-
for (const parentId of parentIds) {
|
|
1796
|
-
const filePath = getTasksFilePath(syncConfig.folder, parentId);
|
|
1797
|
-
try {
|
|
1798
|
-
// Check if file exists
|
|
1799
|
-
if (!await fileExists(filePath)) {
|
|
1800
|
-
failed.push({ parentId, error: `File not found: ${filePath}` });
|
|
1801
|
-
continue;
|
|
1802
|
-
}
|
|
1803
|
-
// Parse local file
|
|
1804
|
-
const content = await readFileContent(filePath);
|
|
1805
|
-
const parsed = parseTasksMarkdown(content);
|
|
1806
|
-
// Get parent work item for area/iteration path
|
|
1807
|
-
const parentWorkItem = await service.getWorkItem(project, parentId);
|
|
1808
|
-
const parentFields = parentWorkItem.fields || {};
|
|
1809
|
-
const areaPath = parentFields['System.AreaPath'];
|
|
1810
|
-
const iterationPath = parentFields['System.IterationPath'];
|
|
1811
|
-
const createdInThisFile = [];
|
|
1812
|
-
// Process each task
|
|
1813
|
-
for (const task of parsed.tasks) {
|
|
1814
|
-
try {
|
|
1815
|
-
if (task.id !== null) {
|
|
1816
|
-
// Existing task - update (auto-converts HTML fields unless skipAutoConvert)
|
|
1817
|
-
const currentTask = await service.getWorkItem(project, task.id);
|
|
1818
|
-
const { operations, fieldsUpdated, convertedFields } = buildTaskPatchOperations(task, currentTask, skipAutoConvert);
|
|
1819
|
-
if (operations.length > 0) {
|
|
1820
|
-
await service.updateWorkItem(project, task.id, operations);
|
|
1821
|
-
updated.push({
|
|
1822
|
-
id: task.id,
|
|
1823
|
-
parentId,
|
|
1824
|
-
fieldsUpdated,
|
|
1825
|
-
...(convertedFields.length > 0 ? { convertedFields } : {}),
|
|
1826
|
-
});
|
|
1827
|
-
}
|
|
1828
|
-
else {
|
|
1829
|
-
// No changes needed
|
|
1830
|
-
updated.push({
|
|
1831
|
-
id: task.id,
|
|
1832
|
-
parentId,
|
|
1833
|
-
fieldsUpdated: [],
|
|
1834
|
-
});
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
else {
|
|
1838
|
-
// New task - create
|
|
1839
|
-
if (!task.title) {
|
|
1840
|
-
continue; // Skip tasks without title
|
|
1841
|
-
}
|
|
1842
|
-
const fields = buildNewTaskFields(task, areaPath, iterationPath);
|
|
1843
|
-
const createdTask = await service.createWorkItem(project, 'Task', fields, parentId);
|
|
1844
|
-
created.push({
|
|
1845
|
-
id: createdTask.id,
|
|
1846
|
-
parentId,
|
|
1847
|
-
title: task.title,
|
|
1848
|
-
});
|
|
1849
|
-
createdInThisFile.push({
|
|
1850
|
-
title: task.title,
|
|
1851
|
-
id: createdTask.id,
|
|
1852
|
-
});
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
catch (taskError) {
|
|
1856
|
-
failed.push({
|
|
1857
|
-
parentId,
|
|
1858
|
-
taskId: task.id || undefined,
|
|
1859
|
-
error: taskError.message,
|
|
1860
|
-
});
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
// Update the file with new task IDs if any were created
|
|
1864
|
-
if (createdInThisFile.length > 0) {
|
|
1865
|
-
const updatedContent = updateTasksFileAfterCreate(content, createdInThisFile);
|
|
1866
|
-
await writeWorkItemFile(filePath, updatedContent);
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
catch (error) {
|
|
1870
|
-
failed.push({ parentId, error: error.message });
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
const result = {
|
|
1874
|
-
updated,
|
|
1875
|
-
created,
|
|
1876
|
-
failed,
|
|
1877
|
-
folder: syncConfig.folder,
|
|
1878
|
-
};
|
|
1879
|
-
return {
|
|
1880
|
-
content: [{
|
|
1881
|
-
type: "text",
|
|
1882
|
-
text: `Pushed tasks to ADO (${updated.length} updated, ${created.length} created):\n\n${JSON.stringify(result, null, 2)}`,
|
|
1883
|
-
}],
|
|
1884
|
-
};
|
|
1885
|
-
}
|
|
1886
|
-
catch (error) {
|
|
1887
|
-
console.error("Error syncing tasks from files:", error);
|
|
1888
|
-
return {
|
|
1889
|
-
content: [{
|
|
1890
|
-
type: "text",
|
|
1891
|
-
text: `Failed to push tasks: ${error.message}`,
|
|
1892
|
-
}],
|
|
1893
|
-
};
|
|
1894
|
-
}
|
|
1895
|
-
});
|
|
1896
|
-
// Log registration summary (enablePullRequestWrite already defined above)
|
|
1897
|
-
// Tool count breakdown:
|
|
1898
|
-
// - Wiki: 6 (get-wikis, search-wiki-pages, get-wiki-page, create-wiki-page, update-wiki-page, str-replace-wiki-page)
|
|
1899
|
-
// - 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)
|
|
1900
|
-
// - Variable Group: 2 (list-variable-groups, get-variable-group)
|
|
1901
|
-
// - PR Read-only: 6 (list-repositories, list-pull-requests, get-pull-request, get-pull-request-threads, get-pull-request-commits, get-pull-request-changes)
|
|
1902
|
-
// - Build Troubleshooting: 3 (get-build-status, get-build-timeline, get-build-logs)
|
|
1903
|
-
// - 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)
|
|
1904
|
-
// - Task Sync: 2 (sync-tasks-to-file, sync-tasks-from-file)
|
|
1905
|
-
// - Config: 1 (get-configuration)
|
|
1906
|
-
// Total base: 6 + 7 + 2 + 6 + 3 + 5 + 2 + 1 = 32
|
|
1907
|
-
// + PR Write: 7 (add-pull-request-thread, create-pull-request, update-pull-request, complete-pull-request, add-pr-reviewer, vote-pull-request, reply-to-pr-thread) - conditional
|
|
1908
|
-
const baseToolsCount = 32;
|
|
1909
|
-
const prWriteToolsCount = enablePullRequestWrite ? 7 : 0;
|
|
1910
|
-
const totalToolsCount = baseToolsCount + prWriteToolsCount;
|
|
1911
|
-
console.error(`azure-devops tools registered: ${totalToolsCount} tools (${baseToolsCount} base + ${prWriteToolsCount} PR write), 4 prompts`);
|
|
1912
|
-
// NOTE: Admin tools (pipelines, service connections, agent pools, environments)
|
|
1913
|
-
// have been moved to the @mcp-consultant-tools/azure-devops-admin package
|
|
51
|
+
// Lazy service singletons
|
|
52
|
+
let wiki = null;
|
|
53
|
+
let workItem = null;
|
|
54
|
+
let pullRequest = null;
|
|
55
|
+
let build = null;
|
|
56
|
+
let variableGroup = null;
|
|
57
|
+
let sync = null;
|
|
58
|
+
let configuration = null;
|
|
59
|
+
return {
|
|
60
|
+
get client() { return getClient(); },
|
|
61
|
+
get wiki() { return wiki ??= new WikiService(getClient()); },
|
|
62
|
+
get workItem() { return workItem ??= new WorkItemService(getClient()); },
|
|
63
|
+
get pullRequest() { return pullRequest ??= new PullRequestService(getClient()); },
|
|
64
|
+
get build() { return build ??= new BuildService(getClient()); },
|
|
65
|
+
get variableGroup() { return variableGroup ??= new VariableGroupService(getClient()); },
|
|
66
|
+
get sync() { return sync ??= new SyncService(workItem ??= new WorkItemService(getClient())); },
|
|
67
|
+
get configuration() { return configuration ??= new ConfigurationService(); },
|
|
68
|
+
};
|
|
1914
69
|
}
|
|
1915
70
|
/**
|
|
1916
|
-
*
|
|
71
|
+
* Register azure-devops tools and prompts to an MCP server.
|
|
72
|
+
* Backward-compatible API for the meta package.
|
|
1917
73
|
*/
|
|
1918
|
-
export
|
|
74
|
+
export function registerAzureDevOpsTools(server) {
|
|
75
|
+
const ctx = createServiceContext();
|
|
76
|
+
registerAllTools(server, ctx);
|
|
77
|
+
registerAllPrompts(server, ctx);
|
|
78
|
+
}
|
|
79
|
+
// Backward-compatible exports
|
|
80
|
+
export { AzureDevOpsClient } from './azure-devops-client.js';
|
|
1919
81
|
/**
|
|
1920
82
|
* Standalone CLI server (when run directly)
|
|
1921
83
|
* Uses realpathSync to resolve symlinks created by npx
|