@radholm/azure-devops-mcp 1.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/LICENSE.md +21 -0
- package/README.md +365 -0
- package/dist/auth.js +129 -0
- package/dist/index.js +140 -0
- package/dist/logger.js +34 -0
- package/dist/org-tenants.js +76 -0
- package/dist/prompts.js +20 -0
- package/dist/shared/content-safety.js +24 -0
- package/dist/shared/domains.js +130 -0
- package/dist/shared/elicitations.js +72 -0
- package/dist/shared/tool-validation.js +92 -0
- package/dist/tools/advanced-security.js +108 -0
- package/dist/tools/auth.js +61 -0
- package/dist/tools/core.js +103 -0
- package/dist/tools/mcp-apps.js +22 -0
- package/dist/tools/pipelines.js +415 -0
- package/dist/tools/repositories.js +1759 -0
- package/dist/tools/search.js +196 -0
- package/dist/tools/test-plans.js +523 -0
- package/dist/tools/wiki.js +392 -0
- package/dist/tools/work-items.js +1316 -0
- package/dist/tools/work.js +391 -0
- package/dist/tools.js +31 -0
- package/dist/useragent.js +20 -0
- package/dist/utils.js +118 -0
- package/dist/version.js +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { TreeStructureGroup, TreeNodeStructureType } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
5
|
+
import { elicitProject, elicitTeam } from "../shared/elicitations.js";
|
|
6
|
+
const WORK_TOOLS = {
|
|
7
|
+
list_team_iterations: "work_list_team_iterations",
|
|
8
|
+
list_iterations: "work_list_iterations",
|
|
9
|
+
create_iterations: "work_create_iterations",
|
|
10
|
+
assign_iterations: "work_assign_iterations",
|
|
11
|
+
get_team_capacity: "work_get_team_capacity",
|
|
12
|
+
update_team_capacity: "work_update_team_capacity",
|
|
13
|
+
get_iteration_capacities: "work_get_iteration_capacities",
|
|
14
|
+
get_team_settings: "work_get_team_settings",
|
|
15
|
+
};
|
|
16
|
+
function configureWorkTools(server, _, connectionProvider) {
|
|
17
|
+
server.tool(WORK_TOOLS.list_team_iterations, "Retrieve a list of iterations for a specific team in a project. If a project or team is not specified, you will be prompted to select one.", {
|
|
18
|
+
project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
19
|
+
team: z.string().optional().describe("The name or ID of the Azure DevOps team. Reuse from prior context if already known. If not provided, a team selection prompt will be shown."),
|
|
20
|
+
timeframe: z.enum(["current"]).optional().describe("The timeframe for which to retrieve iterations. Currently, only 'current' is supported."),
|
|
21
|
+
}, async ({ project, team, timeframe }) => {
|
|
22
|
+
try {
|
|
23
|
+
const connection = await connectionProvider();
|
|
24
|
+
let resolvedProject = project;
|
|
25
|
+
if (!resolvedProject) {
|
|
26
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to list team iterations for.");
|
|
27
|
+
if ("response" in result)
|
|
28
|
+
return result.response;
|
|
29
|
+
resolvedProject = result.resolved;
|
|
30
|
+
}
|
|
31
|
+
let resolvedTeam = team;
|
|
32
|
+
if (!resolvedTeam) {
|
|
33
|
+
const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to list iterations for.");
|
|
34
|
+
if ("response" in result)
|
|
35
|
+
return result.response;
|
|
36
|
+
resolvedTeam = result.resolved;
|
|
37
|
+
}
|
|
38
|
+
const workApi = await connection.getWorkApi();
|
|
39
|
+
const iterations = await workApi.getTeamIterations({ project: resolvedProject, team: resolvedTeam }, timeframe);
|
|
40
|
+
if (!iterations) {
|
|
41
|
+
return { content: [{ type: "text", text: "No iterations found" }], isError: true };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{ type: "text", text: `Project: ${resolvedProject}, Team: ${resolvedTeam}` },
|
|
46
|
+
{ type: "text", text: JSON.stringify(iterations, null, 2) },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: `Error fetching team iterations: ${errorMessage}` }],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
server.tool(WORK_TOOLS.create_iterations, "Create new iterations in a specified Azure DevOps project.", {
|
|
59
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
60
|
+
iterations: z
|
|
61
|
+
.array(z.object({
|
|
62
|
+
iterationName: z.string().describe("The name of the iteration to create."),
|
|
63
|
+
startDate: z.string().optional().describe("The start date of the iteration in ISO format (e.g., '2023-01-01T00:00:00Z'). Optional."),
|
|
64
|
+
finishDate: z.string().optional().describe("The finish date of the iteration in ISO format (e.g., '2023-01-31T23:59:59Z'). Optional."),
|
|
65
|
+
}))
|
|
66
|
+
.describe("An array of iterations to create. Each iteration must have a name and can optionally have start and finish dates in ISO format."),
|
|
67
|
+
}, async ({ project, iterations }) => {
|
|
68
|
+
try {
|
|
69
|
+
const connection = await connectionProvider();
|
|
70
|
+
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const { iterationName, startDate, finishDate } of iterations) {
|
|
73
|
+
// Step 1: Create the iteration
|
|
74
|
+
const iteration = await workItemTrackingApi.createOrUpdateClassificationNode({
|
|
75
|
+
name: iterationName,
|
|
76
|
+
attributes: {
|
|
77
|
+
startDate: startDate ? new Date(startDate) : undefined,
|
|
78
|
+
finishDate: finishDate ? new Date(finishDate) : undefined,
|
|
79
|
+
},
|
|
80
|
+
}, project, TreeStructureGroup.Iterations);
|
|
81
|
+
if (iteration) {
|
|
82
|
+
results.push(iteration);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (results.length === 0) {
|
|
86
|
+
return { content: [{ type: "text", text: "No iterations were created" }], isError: true };
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: `Error creating iterations: ${errorMessage}` }],
|
|
96
|
+
isError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
server.tool(WORK_TOOLS.list_iterations, "List all iterations in a specified Azure DevOps project. If a project is not specified, you will be prompted to select one.", {
|
|
101
|
+
project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
102
|
+
depth: z.coerce.number().default(2).describe("Depth of children to fetch."),
|
|
103
|
+
excludedIds: z.array(z.coerce.number().min(1)).optional().describe("An optional array of iteration IDs, and thier children, that should not be returned."),
|
|
104
|
+
}, async ({ project, depth, excludedIds: ids }) => {
|
|
105
|
+
try {
|
|
106
|
+
const connection = await connectionProvider();
|
|
107
|
+
let resolvedProject = project;
|
|
108
|
+
if (!resolvedProject) {
|
|
109
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to list iterations for.");
|
|
110
|
+
if ("response" in result)
|
|
111
|
+
return result.response;
|
|
112
|
+
resolvedProject = result.resolved;
|
|
113
|
+
}
|
|
114
|
+
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
115
|
+
let results = [];
|
|
116
|
+
if (depth === undefined) {
|
|
117
|
+
depth = 1;
|
|
118
|
+
}
|
|
119
|
+
results = await workItemTrackingApi.getClassificationNodes(resolvedProject, [], depth);
|
|
120
|
+
// Handle null or undefined results
|
|
121
|
+
if (!results) {
|
|
122
|
+
return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
|
|
123
|
+
}
|
|
124
|
+
// Filter out items with structureType=0 (Area nodes), only keep structureType=1 (Iteration nodes)
|
|
125
|
+
let filteredResults = results.filter((node) => node.structureType === TreeNodeStructureType.Iteration);
|
|
126
|
+
// If specific IDs are provided, filter them out recursively (exclude matching nodes and their children)
|
|
127
|
+
if (ids && ids.length > 0) {
|
|
128
|
+
const filterOutIds = (nodes) => {
|
|
129
|
+
return nodes
|
|
130
|
+
.filter((node) => !node.id || !ids.includes(node.id))
|
|
131
|
+
.map((node) => {
|
|
132
|
+
if (node.children && node.children.length > 0) {
|
|
133
|
+
return {
|
|
134
|
+
...node,
|
|
135
|
+
children: filterOutIds(node.children),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return node;
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
filteredResults = filterOutIds(filteredResults);
|
|
142
|
+
}
|
|
143
|
+
if (filteredResults.length === 0) {
|
|
144
|
+
return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: "text", text: JSON.stringify(filteredResults, null, 2) }],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: "text", text: `Error fetching iterations: ${errorMessage}` }],
|
|
154
|
+
isError: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
server.tool(WORK_TOOLS.assign_iterations, "Assign existing iterations to a specific team in a project.", {
|
|
159
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
160
|
+
team: z.string().describe("The name or ID of the Azure DevOps team."),
|
|
161
|
+
iterations: z
|
|
162
|
+
.array(z.object({
|
|
163
|
+
identifier: z.string().describe("The identifier of the iteration to assign."),
|
|
164
|
+
path: z.string().describe("The path of the iteration to assign, e.g., 'Project/Iteration'."),
|
|
165
|
+
}))
|
|
166
|
+
.describe("An array of iterations to assign. Each iteration must have an identifier and a path."),
|
|
167
|
+
}, async ({ project, team, iterations }) => {
|
|
168
|
+
try {
|
|
169
|
+
const connection = await connectionProvider();
|
|
170
|
+
const workApi = await connection.getWorkApi();
|
|
171
|
+
const teamContext = { project, team };
|
|
172
|
+
const results = [];
|
|
173
|
+
for (const { identifier, path } of iterations) {
|
|
174
|
+
const assignment = await workApi.postTeamIteration({ path: path, id: identifier }, teamContext);
|
|
175
|
+
if (assignment) {
|
|
176
|
+
results.push(assignment);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (results.length === 0) {
|
|
180
|
+
return { content: [{ type: "text", text: "No iterations were assigned to the team" }], isError: true };
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text", text: `Error assigning iterations: ${errorMessage}` }],
|
|
190
|
+
isError: true,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
server.tool(WORK_TOOLS.get_team_capacity, "Get the team capacity of a specific team and iteration in a project. If a project is not specified, you will be prompted to select one.", {
|
|
195
|
+
project: z.string().optional().describe("The name or Id of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
196
|
+
team: z.string().describe("The name or Id of the Azure DevOps team. Reuse from prior context if already known."),
|
|
197
|
+
iterationId: z.string().describe("The Iteration Id to get capacity for."),
|
|
198
|
+
}, async ({ project, team, iterationId }) => {
|
|
199
|
+
try {
|
|
200
|
+
const connection = await connectionProvider();
|
|
201
|
+
let resolvedProject = project;
|
|
202
|
+
if (!resolvedProject) {
|
|
203
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to get team capacity for.");
|
|
204
|
+
if ("response" in result)
|
|
205
|
+
return result.response;
|
|
206
|
+
resolvedProject = result.resolved;
|
|
207
|
+
}
|
|
208
|
+
const workApi = await connection.getWorkApi();
|
|
209
|
+
const teamContext = { project: resolvedProject, team };
|
|
210
|
+
const rawResults = await workApi.getCapacitiesWithIdentityRefAndTotals(teamContext, iterationId);
|
|
211
|
+
if (!rawResults || rawResults.teamMembers?.length === 0) {
|
|
212
|
+
return { content: [{ type: "text", text: "No team capacity assigned to the team" }], isError: true };
|
|
213
|
+
}
|
|
214
|
+
// Remove unwanted fields from teamMember and url
|
|
215
|
+
const simplifiedResults = {
|
|
216
|
+
...rawResults,
|
|
217
|
+
teamMembers: (rawResults.teamMembers || []).map((member) => {
|
|
218
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
219
|
+
const { url, ...rest } = member;
|
|
220
|
+
return {
|
|
221
|
+
...rest,
|
|
222
|
+
teamMember: member.teamMember
|
|
223
|
+
? {
|
|
224
|
+
displayName: member.teamMember.displayName,
|
|
225
|
+
id: member.teamMember.id,
|
|
226
|
+
uniqueName: member.teamMember.uniqueName,
|
|
227
|
+
}
|
|
228
|
+
: undefined,
|
|
229
|
+
};
|
|
230
|
+
}),
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: "text", text: JSON.stringify(simplifiedResults, null, 2) }],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text: `Error getting team capacity: ${errorMessage}` }],
|
|
240
|
+
isError: true,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
server.tool(WORK_TOOLS.update_team_capacity, "Update the team capacity of a team member for a specific iteration in a project.", {
|
|
245
|
+
project: z.string().describe("The name or Id of the Azure DevOps project."),
|
|
246
|
+
team: z.string().describe("The name or Id of the Azure DevOps team."),
|
|
247
|
+
teamMemberId: z.string().describe("The team member Id for the specific team member."),
|
|
248
|
+
iterationId: z.string().describe("The Iteration Id to update the capacity for."),
|
|
249
|
+
activities: z
|
|
250
|
+
.array(z.object({
|
|
251
|
+
name: z.string().describe("The name of the activity (e.g., 'Development')."),
|
|
252
|
+
capacityPerDay: z.number().describe("The capacity per day for this activity."),
|
|
253
|
+
}))
|
|
254
|
+
.describe("Array of activities and their daily capacities for the team member."),
|
|
255
|
+
daysOff: z
|
|
256
|
+
.array(z.object({
|
|
257
|
+
start: z.string().describe("Start date of the day off in ISO format."),
|
|
258
|
+
end: z.string().describe("End date of the day off in ISO format."),
|
|
259
|
+
}))
|
|
260
|
+
.optional()
|
|
261
|
+
.describe("Array of days off for the team member, each with a start and end date in ISO format."),
|
|
262
|
+
}, async ({ project, team, teamMemberId, iterationId, activities, daysOff }) => {
|
|
263
|
+
try {
|
|
264
|
+
const connection = await connectionProvider();
|
|
265
|
+
const workApi = await connection.getWorkApi();
|
|
266
|
+
const teamContext = { project, team };
|
|
267
|
+
// Prepare the capacity update object
|
|
268
|
+
const capacityPatch = {
|
|
269
|
+
activities: activities.map((a) => ({
|
|
270
|
+
name: a.name,
|
|
271
|
+
capacityPerDay: a.capacityPerDay,
|
|
272
|
+
})),
|
|
273
|
+
daysOff: (daysOff || []).map((d) => ({
|
|
274
|
+
start: new Date(d.start),
|
|
275
|
+
end: new Date(d.end),
|
|
276
|
+
})),
|
|
277
|
+
};
|
|
278
|
+
// Update the team member's capacity
|
|
279
|
+
const updatedCapacity = await workApi.updateCapacityWithIdentityRef(capacityPatch, teamContext, iterationId, teamMemberId);
|
|
280
|
+
if (!updatedCapacity) {
|
|
281
|
+
return { content: [{ type: "text", text: "Failed to update team member capacity" }], isError: true };
|
|
282
|
+
}
|
|
283
|
+
// Simplify output
|
|
284
|
+
const simplifiedResult = {
|
|
285
|
+
teamMember: updatedCapacity.teamMember
|
|
286
|
+
? {
|
|
287
|
+
displayName: updatedCapacity.teamMember.displayName,
|
|
288
|
+
id: updatedCapacity.teamMember.id,
|
|
289
|
+
uniqueName: updatedCapacity.teamMember.uniqueName,
|
|
290
|
+
}
|
|
291
|
+
: undefined,
|
|
292
|
+
activities: updatedCapacity.activities,
|
|
293
|
+
daysOff: updatedCapacity.daysOff,
|
|
294
|
+
};
|
|
295
|
+
return {
|
|
296
|
+
content: [{ type: "text", text: JSON.stringify(simplifiedResult, null, 2) }],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
301
|
+
return {
|
|
302
|
+
content: [{ type: "text", text: `Error updating team capacity: ${errorMessage}` }],
|
|
303
|
+
isError: true,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
server.tool(WORK_TOOLS.get_iteration_capacities, "Get an iteration's capacity for all teams in iteration and project. If a project is not specified, you will be prompted to select one.", {
|
|
308
|
+
project: z.string().optional().describe("The name or Id of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
309
|
+
iterationId: z.string().describe("The Iteration Id to get capacity for."),
|
|
310
|
+
}, async ({ project, iterationId }) => {
|
|
311
|
+
try {
|
|
312
|
+
const connection = await connectionProvider();
|
|
313
|
+
let resolvedProject = project;
|
|
314
|
+
if (!resolvedProject) {
|
|
315
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to get iteration capacities for.");
|
|
316
|
+
if ("response" in result)
|
|
317
|
+
return result.response;
|
|
318
|
+
resolvedProject = result.resolved;
|
|
319
|
+
}
|
|
320
|
+
const workApi = await connection.getWorkApi();
|
|
321
|
+
const rawResults = await workApi.getTotalIterationCapacities(resolvedProject, iterationId);
|
|
322
|
+
if (!rawResults || !rawResults.teams || rawResults.teams.length === 0) {
|
|
323
|
+
return { content: [{ type: "text", text: "No iteration capacity assigned to the teams" }], isError: true };
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
content: [{ type: "text", text: JSON.stringify(rawResults, null, 2) }],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
331
|
+
return {
|
|
332
|
+
content: [{ type: "text", text: `Error getting iteration capacities: ${errorMessage}` }],
|
|
333
|
+
isError: true,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
server.tool(WORK_TOOLS.get_team_settings, "Get team settings including default iteration, backlog iteration, and default area path for a team. If a project or team is not specified, you will be prompted to select one.", {
|
|
338
|
+
project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
339
|
+
team: z.string().optional().describe("The name or ID of the Azure DevOps team. Reuse from prior context if already known. If not provided, a team selection prompt will be shown."),
|
|
340
|
+
}, async ({ project, team }) => {
|
|
341
|
+
try {
|
|
342
|
+
const connection = await connectionProvider();
|
|
343
|
+
let resolvedProject = project;
|
|
344
|
+
if (!resolvedProject) {
|
|
345
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to get team settings for.");
|
|
346
|
+
if ("response" in result)
|
|
347
|
+
return result.response;
|
|
348
|
+
resolvedProject = result.resolved;
|
|
349
|
+
}
|
|
350
|
+
let resolvedTeam = team;
|
|
351
|
+
if (!resolvedTeam) {
|
|
352
|
+
const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to get settings for.");
|
|
353
|
+
if ("response" in result)
|
|
354
|
+
return result.response;
|
|
355
|
+
resolvedTeam = result.resolved;
|
|
356
|
+
}
|
|
357
|
+
const workApi = await connection.getWorkApi();
|
|
358
|
+
const teamContext = { project: resolvedProject, team: resolvedTeam };
|
|
359
|
+
const teamSettings = await workApi.getTeamSettings(teamContext);
|
|
360
|
+
if (!teamSettings) {
|
|
361
|
+
return { content: [{ type: "text", text: "No team settings found" }], isError: true };
|
|
362
|
+
}
|
|
363
|
+
const teamFieldValues = await workApi.getTeamFieldValues(teamContext);
|
|
364
|
+
const result = {
|
|
365
|
+
backlogIteration: teamSettings.backlogIteration,
|
|
366
|
+
defaultIteration: teamSettings.defaultIteration,
|
|
367
|
+
defaultIterationMacro: teamSettings.defaultIterationMacro,
|
|
368
|
+
backlogVisibilities: teamSettings.backlogVisibilities,
|
|
369
|
+
bugsBehavior: teamSettings.bugsBehavior,
|
|
370
|
+
workingDays: teamSettings.workingDays,
|
|
371
|
+
defaultAreaPath: teamFieldValues?.defaultValue,
|
|
372
|
+
areaPathField: teamFieldValues?.field,
|
|
373
|
+
areaPaths: teamFieldValues?.values,
|
|
374
|
+
};
|
|
375
|
+
return {
|
|
376
|
+
content: [
|
|
377
|
+
{ type: "text", text: `Project: ${resolvedProject}, Team: ${resolvedTeam}` },
|
|
378
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: "text", text: `Error fetching team settings: ${errorMessage}` }],
|
|
386
|
+
isError: true,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
export { WORK_TOOLS, configureWorkTools };
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { Domain } from "./shared/domains.js";
|
|
4
|
+
import { configureAdvSecTools } from "./tools/advanced-security.js";
|
|
5
|
+
import { configureMcpAppsTools } from "./tools/mcp-apps.js";
|
|
6
|
+
import { configurePipelineTools } from "./tools/pipelines.js";
|
|
7
|
+
import { configureCoreTools } from "./tools/core.js";
|
|
8
|
+
import { configureRepoTools } from "./tools/repositories.js";
|
|
9
|
+
import { configureSearchTools } from "./tools/search.js";
|
|
10
|
+
import { configureTestPlanTools } from "./tools/test-plans.js";
|
|
11
|
+
import { configureWikiTools } from "./tools/wiki.js";
|
|
12
|
+
import { configureWorkTools } from "./tools/work.js";
|
|
13
|
+
import { configureWorkItemTools } from "./tools/work-items.js";
|
|
14
|
+
function configureAllTools(server, tokenProvider, connectionProvider, userAgentProvider, enabledDomains) {
|
|
15
|
+
const configureIfDomainEnabled = (domain, configureFn) => {
|
|
16
|
+
if (enabledDomains.has(domain)) {
|
|
17
|
+
configureFn();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
configureIfDomainEnabled(Domain.CORE, () => configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
21
|
+
configureIfDomainEnabled(Domain.MCP_APPS, () => configureMcpAppsTools(server));
|
|
22
|
+
configureIfDomainEnabled(Domain.WORK, () => configureWorkTools(server, tokenProvider, connectionProvider));
|
|
23
|
+
configureIfDomainEnabled(Domain.PIPELINES, () => configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
24
|
+
configureIfDomainEnabled(Domain.REPOSITORIES, () => configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
25
|
+
configureIfDomainEnabled(Domain.WORK_ITEMS, () => configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
26
|
+
configureIfDomainEnabled(Domain.WIKI, () => configureWikiTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
27
|
+
configureIfDomainEnabled(Domain.TEST_PLANS, () => configureTestPlanTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
28
|
+
configureIfDomainEnabled(Domain.SEARCH, () => configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
29
|
+
configureIfDomainEnabled(Domain.ADVANCED_SECURITY, () => configureAdvSecTools(server, tokenProvider, connectionProvider));
|
|
30
|
+
}
|
|
31
|
+
export { configureAllTools };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
class UserAgentComposer {
|
|
4
|
+
_userAgent;
|
|
5
|
+
_mcpClientInfoAppended;
|
|
6
|
+
constructor(packageVersion) {
|
|
7
|
+
this._userAgent = `AzureDevOps.MCP/${packageVersion} (local)`;
|
|
8
|
+
this._mcpClientInfoAppended = false;
|
|
9
|
+
}
|
|
10
|
+
get userAgent() {
|
|
11
|
+
return this._userAgent;
|
|
12
|
+
}
|
|
13
|
+
appendMcpClientInfo(info) {
|
|
14
|
+
if (!this._mcpClientInfoAppended && info && info.name && info.version) {
|
|
15
|
+
this._userAgent += ` ${info.name}/${info.version}`;
|
|
16
|
+
this._mcpClientInfoAppended = true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export { UserAgentComposer };
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
/**
|
|
4
|
+
* Default API versions for Azure DevOps cloud.
|
|
5
|
+
* On-premises Azure DevOps Server may not support these versions.
|
|
6
|
+
* Users can override via the AZURE_DEVOPS_API_VERSION environment variable.
|
|
7
|
+
*
|
|
8
|
+
* Supported on-prem versions:
|
|
9
|
+
* - Azure DevOps Server 2022: up to 7.1
|
|
10
|
+
* - Azure DevOps Server 2020: up to 6.0
|
|
11
|
+
* - Azure DevOps Server 2019: up to 5.1
|
|
12
|
+
*/
|
|
13
|
+
const defaultApiVersion = "7.2-preview.1";
|
|
14
|
+
export const apiVersion = process.env.AZURE_DEVOPS_API_VERSION ?? defaultApiVersion;
|
|
15
|
+
export const batchApiVersion = "5.0";
|
|
16
|
+
export const markdownCommentsApiVersion = process.env.AZURE_DEVOPS_API_VERSION ?? "7.2-preview.4";
|
|
17
|
+
export function createEnumMapping(enumObject) {
|
|
18
|
+
const mapping = {};
|
|
19
|
+
for (const [key, value] of Object.entries(enumObject)) {
|
|
20
|
+
if (typeof key === "string" && typeof value === "number") {
|
|
21
|
+
mapping[key.toLowerCase()] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return mapping;
|
|
25
|
+
}
|
|
26
|
+
export function mapStringToEnum(value, enumObject, defaultValue) {
|
|
27
|
+
if (!value)
|
|
28
|
+
return defaultValue;
|
|
29
|
+
const enumMapping = createEnumMapping(enumObject);
|
|
30
|
+
return enumMapping[value.toLowerCase()] ?? defaultValue;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Maps an array of strings to an array of enum values, filtering out invalid values.
|
|
34
|
+
* @param values Array of string values to map
|
|
35
|
+
* @param enumObject The enum object to map to
|
|
36
|
+
* @returns Array of valid enum values
|
|
37
|
+
*/
|
|
38
|
+
export function mapStringArrayToEnum(values, enumObject) {
|
|
39
|
+
if (!values)
|
|
40
|
+
return [];
|
|
41
|
+
return values.map((value) => mapStringToEnum(value, enumObject)).filter((v) => v !== undefined);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Converts a TypeScript numeric enum to an array of string keys for use with z.enum().
|
|
45
|
+
* This ensures that enum schemas generate string values rather than numeric values.
|
|
46
|
+
* @param enumObject The TypeScript enum object
|
|
47
|
+
* @returns Array of string keys from the enum
|
|
48
|
+
*/
|
|
49
|
+
export function getEnumKeys(enumObject) {
|
|
50
|
+
return Object.keys(enumObject).filter((key) => isNaN(Number(key)));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Safely converts a string enum key to its corresponding enum value.
|
|
54
|
+
* Validates that the key exists in the enum before conversion.
|
|
55
|
+
* @param enumObject The TypeScript enum object
|
|
56
|
+
* @param key The string key to convert
|
|
57
|
+
* @returns The enum value if key is valid, undefined otherwise
|
|
58
|
+
*/
|
|
59
|
+
export function safeEnumConvert(enumObject, key) {
|
|
60
|
+
if (!key)
|
|
61
|
+
return undefined;
|
|
62
|
+
const validKeys = getEnumKeys(enumObject);
|
|
63
|
+
if (!validKeys.includes(key)) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return enumObject[key];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Encodes `>` and `<` for Markdown formatted fields.
|
|
70
|
+
*
|
|
71
|
+
* @param value The text value to encode
|
|
72
|
+
* @param format The format of the field ('Markdown' or 'Html')
|
|
73
|
+
* @returns The encoded text, or original text if format is not Markdown
|
|
74
|
+
*/
|
|
75
|
+
export function encodeFormattedValue(value, format) {
|
|
76
|
+
if (!value || format !== "Markdown")
|
|
77
|
+
return value;
|
|
78
|
+
const result = value.replace(/</g, "<").replace(/>/g, ">");
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Detects whether a string returned from an ADO API stream is actually an error
|
|
83
|
+
* response serialized as JSON (e.g. a 404 GitItemNotFoundException or
|
|
84
|
+
* WikiPageNotFoundException) rather than real content.
|
|
85
|
+
*
|
|
86
|
+
* The ADO Node API client swallows non-2xx HTTP responses and delivers the
|
|
87
|
+
* error body as a stream, so callers must check explicitly after reading.
|
|
88
|
+
*
|
|
89
|
+
* @returns The human-readable error message extracted from the JSON, or null if
|
|
90
|
+
* the content is not an ADO error response.
|
|
91
|
+
*/
|
|
92
|
+
export function extractAdoStreamError(content) {
|
|
93
|
+
try {
|
|
94
|
+
const json = JSON.parse(content.trim());
|
|
95
|
+
if (json && typeof json.typeName === "string" && typeof json.message === "string") {
|
|
96
|
+
return json.message;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Not JSON — not an ADO error response.
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Convert a Node.js ReadableStream to a string.
|
|
106
|
+
* Shared utility for consistent stream handling across tools.
|
|
107
|
+
*/
|
|
108
|
+
export function streamToString(stream) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
let data = "";
|
|
111
|
+
stream.setEncoding("utf8");
|
|
112
|
+
stream.on("data", (chunk) => {
|
|
113
|
+
data += chunk;
|
|
114
|
+
});
|
|
115
|
+
stream.on("error", reject);
|
|
116
|
+
stream.on("end", () => resolve(data));
|
|
117
|
+
});
|
|
118
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const packageVersion = "1.0.0-beta.1";
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@radholm/azure-devops-mcp",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "MCP server for interacting with Azure DevOps",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Fredrik Radholm",
|
|
7
|
+
"homepage": "https://github.com/radholm/azure-devops-mcp",
|
|
8
|
+
"bugs": "https://github.com/radholm/azure-devops-mcp/issues",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/radholm/azure-devops-mcp.git"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"mcp-server-azuredevops": "dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"preinstall": "npm config set registry https://registry.npmjs.org/",
|
|
25
|
+
"prebuild": "node -p \"'export const packageVersion = ' + JSON.stringify(require('./package.json').version) + ';\\n'\" > src/version.ts && prettier --write src/version.ts",
|
|
26
|
+
"validate-tools": "tsc --noEmit && node scripts/build-validate-tools.js",
|
|
27
|
+
"build": "tsc && shx chmod +x dist/*.js",
|
|
28
|
+
"prepare": "npm run build && husky",
|
|
29
|
+
"watch": "tsc --watch",
|
|
30
|
+
"inspect": "ALLOWED_ORIGINS=http://127.0.0.1:6274 npx @modelcontextprotocol/inspector@0.21.0 node dist/index.js",
|
|
31
|
+
"start": "node -r tsconfig-paths/register dist/index.js",
|
|
32
|
+
"eslint": "eslint",
|
|
33
|
+
"eslint-fix": "eslint --fix",
|
|
34
|
+
"format": "prettier --write azure-devops-mcp",
|
|
35
|
+
"format-check": "prettier --check azure-devops-mcp",
|
|
36
|
+
"clean": "shx rm -rf dist",
|
|
37
|
+
"test": "jest"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@azure/identity": "^4.10.0",
|
|
41
|
+
"@azure/msal-node": "^5.0.6",
|
|
42
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
43
|
+
"azure-devops-extension-api": "^4.264.0",
|
|
44
|
+
"azure-devops-extension-sdk": "^4.0.2",
|
|
45
|
+
"azure-devops-node-api": "^15.1.2",
|
|
46
|
+
"winston": "^3.18.3",
|
|
47
|
+
"yargs": "^18.0.0",
|
|
48
|
+
"zod": "^3.25.63",
|
|
49
|
+
"zod-to-json-schema": "^3.24.5"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/jest": "^30.0.0",
|
|
53
|
+
"@types/node": "^22.19.1",
|
|
54
|
+
"eslint-config-prettier": "10.1.8",
|
|
55
|
+
"eslint-plugin-header": "^3.1.1",
|
|
56
|
+
"glob": "^13.0.0",
|
|
57
|
+
"husky": "^9.1.7",
|
|
58
|
+
"jest": "^30.0.2",
|
|
59
|
+
"jest-extended": "^7.0.0",
|
|
60
|
+
"lint-staged": "^17.0.0",
|
|
61
|
+
"prettier": "3.8.3",
|
|
62
|
+
"shx": "^0.4.0",
|
|
63
|
+
"ts-jest": "^29.4.6",
|
|
64
|
+
"tsconfig-paths": "^4.2.0",
|
|
65
|
+
"typescript": "^5.9.3",
|
|
66
|
+
"typescript-eslint": "^8.54.0"
|
|
67
|
+
},
|
|
68
|
+
"lint-staged": {
|
|
69
|
+
"**/*.(js|ts|jsx|tsx|json|css|md)": [
|
|
70
|
+
"npm run format"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|