@azure-devops/mcp 0.1.0
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 +247 -0
- package/dist/index.js +51 -0
- package/dist/prompts.js +25 -0
- package/dist/tools/auth.js +20 -0
- package/dist/tools/builds.js +146 -0
- package/dist/tools/core.js +36 -0
- package/dist/tools/releases.js +72 -0
- package/dist/tools/repos.js +299 -0
- package/dist/tools/search.js +179 -0
- package/dist/tools/testplans.js +212 -0
- package/dist/tools/wiki.js +74 -0
- package/dist/tools/work.js +70 -0
- package/dist/tools/workitems.js +485 -0
- package/dist/tools.js +21 -0
- package/dist/utils.js +6 -0
- package/dist/version.js +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { batchApiVersion, userAgent } from "../utils.js";
|
|
5
|
+
const WORKITEM_TOOLS = {
|
|
6
|
+
my_work_items: "wit_my_work_items",
|
|
7
|
+
list_backlogs: "wit_list_backlogs",
|
|
8
|
+
list_backlog_work_items: "wit_list_backlog_work_items",
|
|
9
|
+
get_work_item: "wit_get_work_item",
|
|
10
|
+
get_work_items_batch_by_ids: "wit_get_work_items_batch_by_ids",
|
|
11
|
+
update_work_item: "wit_update_work_item",
|
|
12
|
+
create_work_item: "wit_create_work_item",
|
|
13
|
+
list_work_item_comments: "wit_list_work_item_comments",
|
|
14
|
+
get_work_items_for_iteration: "wit_get_work_items_for_iteration",
|
|
15
|
+
add_work_item_comment: "wit_add_work_item_comment",
|
|
16
|
+
add_child_work_item: "wit_add_child_work_item",
|
|
17
|
+
link_work_item_to_pull_request: "wit_link_work_item_to_pull_request",
|
|
18
|
+
get_work_item_type: "wit_get_work_item_type",
|
|
19
|
+
get_query: "wit_get_query",
|
|
20
|
+
get_query_results_by_id: "wit_get_query_results_by_id",
|
|
21
|
+
update_work_items_batch: "wit_update_work_items_batch",
|
|
22
|
+
close_and_link_workitem_duplicates: "wit_close_and_link_workitem_duplicates",
|
|
23
|
+
work_items_link: "wit_work_items_link"
|
|
24
|
+
};
|
|
25
|
+
function getLinkTypeFromName(name) {
|
|
26
|
+
switch (name.toLowerCase()) {
|
|
27
|
+
case "parent":
|
|
28
|
+
return "System.LinkTypes.Hierarchy-Reverse";
|
|
29
|
+
case "child":
|
|
30
|
+
return "System.LinkTypes.Hierarchy-Forward";
|
|
31
|
+
case "duplicate":
|
|
32
|
+
return "System.LinkTypes.Duplicate-Forward";
|
|
33
|
+
case "duplicate of":
|
|
34
|
+
return "System.LinkTypes.Duplicate-Reverse";
|
|
35
|
+
case "related":
|
|
36
|
+
return "System.LinkTypes.Related";
|
|
37
|
+
case "successor":
|
|
38
|
+
return "System.LinkTypes.Dependency-Forward";
|
|
39
|
+
case "predecessor":
|
|
40
|
+
return "System.LinkTypes.Dependency-Reverse";
|
|
41
|
+
case "tested by":
|
|
42
|
+
return "Microsoft.VSTS.Common.TestedBy-Forward";
|
|
43
|
+
case "tests":
|
|
44
|
+
return "Microsoft.VSTS.Common.TestedBy-Reverse";
|
|
45
|
+
default:
|
|
46
|
+
throw new Error(`Unknown link type: ${name}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
50
|
+
server.tool(WORKITEM_TOOLS.list_backlogs, "Revieve a list of backlogs for a given project and team.", {
|
|
51
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
52
|
+
team: z.string().describe("The name or ID of the Azure DevOps team.")
|
|
53
|
+
}, async ({ project, team }) => {
|
|
54
|
+
const connection = await connectionProvider();
|
|
55
|
+
const workApi = await connection.getWorkApi();
|
|
56
|
+
const teamContext = { project, team };
|
|
57
|
+
const backlogs = await workApi.getBacklogs(teamContext);
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: JSON.stringify(backlogs, null, 2) }],
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
server.tool(WORKITEM_TOOLS.list_backlog_work_items, "Retrieve a list of backlogs of for a given project, team, and backlog category", {
|
|
63
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
64
|
+
team: z.string().describe("The name or ID of the Azure DevOps team."),
|
|
65
|
+
backlogId: z.string().describe("The ID of the backlog category to retrieve work items from.")
|
|
66
|
+
}, async ({ project, team, backlogId }) => {
|
|
67
|
+
const connection = await connectionProvider();
|
|
68
|
+
const workApi = await connection.getWorkApi();
|
|
69
|
+
const teamContext = { project, team };
|
|
70
|
+
const workItems = await workApi.getBacklogLevelWorkItems(teamContext, backlogId);
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: JSON.stringify(workItems, null, 2) }],
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
server.tool(WORKITEM_TOOLS.my_work_items, "Retrieve a list of work items relevent to the authenticated user.", {
|
|
76
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
77
|
+
type: z.enum(["assignedtome", "myactivity"]).default("assignedtome").describe("The type of work items to retrieve. Defaults to 'assignedtome'."),
|
|
78
|
+
top: z.number().default(50).describe("The maximum number of work items to return. Defaults to 50."),
|
|
79
|
+
includeCompleted: z.boolean().default(false).describe("Whether to include completed work items. Defaults to false."),
|
|
80
|
+
}, async ({ project, type, top, includeCompleted }) => {
|
|
81
|
+
const connection = await connectionProvider();
|
|
82
|
+
const workApi = await connection.getWorkApi();
|
|
83
|
+
const workItems = await workApi.getPredefinedQueryResults(project, type, top, includeCompleted);
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: JSON.stringify(workItems, null, 2) }],
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
server.tool(WORKITEM_TOOLS.get_work_items_batch_by_ids, "Retrieve list of work items by IDs in batch.", {
|
|
89
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
90
|
+
ids: z.array(z.number()).describe("The IDs of the work items to retrieve.")
|
|
91
|
+
}, async ({ project, ids }) => {
|
|
92
|
+
const connection = await connectionProvider();
|
|
93
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
94
|
+
const fields = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags"];
|
|
95
|
+
const workitems = await workItemApi.getWorkItemsBatch({ ids, fields }, project);
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: "text", text: JSON.stringify(workitems, null, 2) }],
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
server.tool(WORKITEM_TOOLS.get_work_item, "Get a single work item by ID.", {
|
|
101
|
+
id: z.number().describe("The ID of the work item to retrieve."),
|
|
102
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
103
|
+
fields: z.array(z.string()).optional().describe("Optional list of fields to include in the response. If not provided, all fields will be returned."),
|
|
104
|
+
asOf: z.date().optional().describe("Optional date to retrieve the work item as of a specific time. If not provided, the current state will be returned."),
|
|
105
|
+
expand: z
|
|
106
|
+
.enum(["all", "fields", "links", "none", "relations"])
|
|
107
|
+
.describe("Optional expand parameter to include additional details in the response.")
|
|
108
|
+
.optional().describe("Expand options include 'all', 'fields', 'links', 'none', and 'relations'. Defaults to 'none'."),
|
|
109
|
+
}, async ({ id, project, fields, asOf, expand }) => {
|
|
110
|
+
const connection = await connectionProvider();
|
|
111
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
112
|
+
const workItem = await workItemApi.getWorkItem(id, fields, asOf, expand, project);
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
server.tool(WORKITEM_TOOLS.list_work_item_comments, "Retrieve list of comments for a work item by ID.", {
|
|
118
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
119
|
+
workItemId: z.number().describe("The ID of the work item to retrieve comments for."),
|
|
120
|
+
top: z.number().default(50).describe("Optional number of comments to retrieve. Defaults to all comments.")
|
|
121
|
+
}, async ({ project, workItemId, top }) => {
|
|
122
|
+
const connection = await connectionProvider();
|
|
123
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
124
|
+
const comments = await workItemApi.getComments(project, workItemId, top);
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: "text", text: JSON.stringify(comments, null, 2) }],
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
server.tool(WORKITEM_TOOLS.add_work_item_comment, "Add comment to a work item by ID.", {
|
|
130
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
131
|
+
workItemId: z.number().describe("The ID of the work item to add a comment to."),
|
|
132
|
+
comment: z.string().describe("The text of the comment to add to the work item.")
|
|
133
|
+
}, async ({ project, workItemId, comment }) => {
|
|
134
|
+
const connection = await connectionProvider();
|
|
135
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
136
|
+
const commentCreate = { text: comment };
|
|
137
|
+
const commentResponse = await workItemApi.addComment(commentCreate, project, workItemId);
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{ type: "text", text: JSON.stringify(commentResponse, null, 2) },
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
server.tool(WORKITEM_TOOLS.add_child_work_item, "Create a child work item from a parent by ID.", {
|
|
145
|
+
parentId: z.number().describe("The ID of the parent work item to create a child work item under."),
|
|
146
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
147
|
+
workItemType: z.string().describe("The type of the child work item to create."),
|
|
148
|
+
title: z.string().describe("The title of the child work item."),
|
|
149
|
+
description: z.string().describe("The description of the child work item."),
|
|
150
|
+
areaPath: z.string().optional().describe("Optional area path for the child work item."),
|
|
151
|
+
iterationPath: z.string().optional().describe("Optional iteration path for the child work item."),
|
|
152
|
+
}, async ({ parentId, project, workItemType, title, description, areaPath, iterationPath, }) => {
|
|
153
|
+
const connection = await connectionProvider();
|
|
154
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
155
|
+
const document = [
|
|
156
|
+
{
|
|
157
|
+
op: "add",
|
|
158
|
+
path: "/fields/System.Title",
|
|
159
|
+
value: title
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
op: "add",
|
|
163
|
+
path: "/fields/System.Description",
|
|
164
|
+
value: description
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
op: "add",
|
|
168
|
+
path: "/relations/-",
|
|
169
|
+
value: {
|
|
170
|
+
rel: "System.LinkTypes.Hierarchy-Reverse",
|
|
171
|
+
url: `${connection.serverUrl}/${project}/_apis/wit/workItems/${parentId}`,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
if (areaPath && areaPath.trim().length > 0) {
|
|
176
|
+
document.push({
|
|
177
|
+
op: "add",
|
|
178
|
+
path: "/fields/System.AreaPath",
|
|
179
|
+
value: areaPath,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (iterationPath && iterationPath.trim().length > 0) {
|
|
183
|
+
document.push({
|
|
184
|
+
op: "add",
|
|
185
|
+
path: "/fields/System.IterationPath",
|
|
186
|
+
value: iterationPath,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const childWorkItem = await workItemApi.createWorkItem(null, document, project, workItemType);
|
|
190
|
+
return {
|
|
191
|
+
content: [
|
|
192
|
+
{ type: "text", text: JSON.stringify(childWorkItem, null, 2) },
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
server.tool(WORKITEM_TOOLS.link_work_item_to_pull_request, "Link a single work item to an existing pull request.", {
|
|
197
|
+
project: z.string().describe,
|
|
198
|
+
repositoryId: z.string().describe("The ID of the repository containing the pull request. Do not use the repository name here, use the ID instead."),
|
|
199
|
+
pullRequestId: z.number().describe("The ID of the pull request to link to."),
|
|
200
|
+
workItemId: z.number().describe("The ID of the work item to link to the pull request."),
|
|
201
|
+
}, async ({ project, repositoryId, pullRequestId, workItemId }) => {
|
|
202
|
+
const connection = await connectionProvider();
|
|
203
|
+
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
204
|
+
try {
|
|
205
|
+
// Create artifact link relation using vstfs format
|
|
206
|
+
// Format: vstfs:///Git/PullRequestId/{project}/{repositoryId}/{pullRequestId}
|
|
207
|
+
const artifactPathValue = `${project}/${repositoryId}/${pullRequestId}`;
|
|
208
|
+
const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`;
|
|
209
|
+
// Use the PATCH document format for adding a relation
|
|
210
|
+
const patchDocument = [
|
|
211
|
+
{
|
|
212
|
+
op: "add",
|
|
213
|
+
path: "/relations/-",
|
|
214
|
+
value: {
|
|
215
|
+
rel: "ArtifactLink",
|
|
216
|
+
url: vstfsUrl,
|
|
217
|
+
attributes: {
|
|
218
|
+
name: "Pull Request",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
// Use the WorkItem API to update the work item with the new relation
|
|
224
|
+
await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, project);
|
|
225
|
+
return {
|
|
226
|
+
content: [
|
|
227
|
+
{
|
|
228
|
+
type: "text",
|
|
229
|
+
text: JSON.stringify({
|
|
230
|
+
workItemId,
|
|
231
|
+
pullRequestId,
|
|
232
|
+
success: true,
|
|
233
|
+
}, null, 2),
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
console.error(`Error linking work item ${workItemId} to PR ${pullRequestId}:`, error);
|
|
240
|
+
return {
|
|
241
|
+
content: [
|
|
242
|
+
{
|
|
243
|
+
type: "text",
|
|
244
|
+
text: JSON.stringify({
|
|
245
|
+
workItemId,
|
|
246
|
+
pullRequestId,
|
|
247
|
+
success: false,
|
|
248
|
+
}, null, 2),
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
server.tool(WORKITEM_TOOLS.get_work_items_for_iteration, "Retrieve a list of work items for a specified iteration.", {
|
|
255
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
256
|
+
team: z.string().optional().describe("The name or ID of the Azure DevOps team. If not provided, the default team will be used."),
|
|
257
|
+
iterationId: z.string().describe("The ID of the iteration to retrieve work items for."),
|
|
258
|
+
}, async ({ project, team, iterationId }) => {
|
|
259
|
+
const connection = await connectionProvider();
|
|
260
|
+
const workApi = await connection.getWorkApi();
|
|
261
|
+
//get the work items for the current iteration
|
|
262
|
+
const workItems = await workApi.getIterationWorkItems({ project, team }, iterationId);
|
|
263
|
+
return {
|
|
264
|
+
content: [{ type: "text", text: JSON.stringify(workItems, null, 2) }],
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
server.tool(WORKITEM_TOOLS.update_work_item, "Update a work item by ID with specified fields.", {
|
|
268
|
+
id: z.number().describe("The ID of the work item to update."),
|
|
269
|
+
updates: z.array(z.object({
|
|
270
|
+
op: z.enum(["add", "replace", "remove"]).default("add").describe("The operation to perform on the field."),
|
|
271
|
+
path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
|
|
272
|
+
value: z.string().describe("The new value for the field. This is required for 'add' and 'replace' operations, and should be omitted for 'remove' operations."),
|
|
273
|
+
})).describe("An array of field updates to apply to the work item."),
|
|
274
|
+
}, async ({ id, updates }) => {
|
|
275
|
+
const connection = await connectionProvider();
|
|
276
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
277
|
+
const updatedWorkItem = await workItemApi.updateWorkItem(null, updates, id);
|
|
278
|
+
return {
|
|
279
|
+
content: [
|
|
280
|
+
{ type: "text", text: JSON.stringify(updatedWorkItem, null, 2) },
|
|
281
|
+
],
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
server.tool(WORKITEM_TOOLS.get_work_item_type, "Get a specific work item type.", {
|
|
285
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
286
|
+
workItemType: z.string().describe("The name of the work item type to retrieve."),
|
|
287
|
+
}, async ({ project, workItemType }) => {
|
|
288
|
+
const connection = await connectionProvider();
|
|
289
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
290
|
+
const workItemTypeInfo = await workItemApi.getWorkItemType(project, workItemType);
|
|
291
|
+
return {
|
|
292
|
+
content: [
|
|
293
|
+
{ type: "text", text: JSON.stringify(workItemTypeInfo, null, 2) },
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
server.tool(WORKITEM_TOOLS.create_work_item, "Create a new work item in a specified project and work item type.", {
|
|
298
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
299
|
+
workItemType: z.string().describe("The type of work item to create, e.g., 'Task', 'Bug', etc."),
|
|
300
|
+
fields: z.record(z.string(), z.string()).describe("A record of field names and values to set on the new work item. Each key is a field name, and each value is the corresponding value to set for that field."),
|
|
301
|
+
}, async ({ project, workItemType, fields }) => {
|
|
302
|
+
const connection = await connectionProvider();
|
|
303
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
304
|
+
const document = Object.entries(fields).map(([key, value]) => ({
|
|
305
|
+
op: "add",
|
|
306
|
+
path: `/fields/${key}`,
|
|
307
|
+
value,
|
|
308
|
+
}));
|
|
309
|
+
const newWorkItem = await workItemApi.createWorkItem(null, document, project, workItemType);
|
|
310
|
+
return {
|
|
311
|
+
content: [{ type: "text", text: JSON.stringify(newWorkItem, null, 2) }],
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
server.tool(WORKITEM_TOOLS.get_query, "Get a query by its ID or path.", {
|
|
315
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
316
|
+
query: z.string().describe("The ID or path of the query to retrieve."),
|
|
317
|
+
expand: z.enum(["all", "clauses", "minimal", "none", "wiql"]).optional().describe("Optional expand parameter to include additional details in the response. Defaults to 'none'."),
|
|
318
|
+
depth: z.number().default(0).describe("Optional depth parameter to specify how deep to expand the query. Defaults to 0."),
|
|
319
|
+
includeDeleted: z.boolean().default(false).describe("Whether to include deleted items in the query results. Defaults to false."),
|
|
320
|
+
useIsoDateFormat: z.boolean().default(false).describe("Whether to use ISO date format in the response. Defaults to false."),
|
|
321
|
+
}, async ({ project, query, expand, depth, includeDeleted, useIsoDateFormat, }) => {
|
|
322
|
+
const connection = await connectionProvider();
|
|
323
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
324
|
+
const queryDetails = await workItemApi.getQuery(project, query, expand, depth, includeDeleted, useIsoDateFormat);
|
|
325
|
+
return {
|
|
326
|
+
content: [
|
|
327
|
+
{ type: "text", text: JSON.stringify(queryDetails, null, 2) },
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
});
|
|
331
|
+
server.tool(WORKITEM_TOOLS.get_query_results_by_id, "Retrieve the results of a work item query given the query ID.", {
|
|
332
|
+
id: z.string().describe("The ID of the query to retrieve results for."),
|
|
333
|
+
project: z.string().optional().describe("The name or ID of the Azure DevOps project. If not provided, the default project will be used."),
|
|
334
|
+
team: z.string().optional().describe("The name or ID of the Azure DevOps team. If not provided, the default team will be used."),
|
|
335
|
+
timePrecision: z.boolean().optional().describe("Whether to include time precision in the results. Defaults to false."),
|
|
336
|
+
top: z.number().default(50).describe("The maximum number of results to return. Defaults to 50."),
|
|
337
|
+
}, async ({ id, project, team, timePrecision, top }) => {
|
|
338
|
+
const connection = await connectionProvider();
|
|
339
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
340
|
+
const teamContext = { project, team };
|
|
341
|
+
const queryResult = await workItemApi.queryById(id, teamContext, timePrecision, top);
|
|
342
|
+
return {
|
|
343
|
+
content: [{ type: "text", text: JSON.stringify(queryResult, null, 2) }],
|
|
344
|
+
};
|
|
345
|
+
});
|
|
346
|
+
server.tool(WORKITEM_TOOLS.update_work_items_batch, "Update work items in batch", {
|
|
347
|
+
updates: z.array(z.object({
|
|
348
|
+
op: z.enum(["add", "replace", "remove"]).default("add").describe("The operation to perform on the field."),
|
|
349
|
+
id: z.number().describe("The ID of the work item to update."),
|
|
350
|
+
path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
|
|
351
|
+
value: z.string().describe("The new value for the field. This is required for 'add' and 'replace' operations, and should be omitted for 'remove' operations."),
|
|
352
|
+
})).describe("An array of updates to apply to work items. Each update should include the operation (op), work item ID (id), field path (path), and new value (value)."),
|
|
353
|
+
}, async ({ updates }) => {
|
|
354
|
+
const connection = await connectionProvider();
|
|
355
|
+
const orgUrl = connection.serverUrl;
|
|
356
|
+
const accessToken = await tokenProvider();
|
|
357
|
+
// Extract unique IDs from the updates array
|
|
358
|
+
const uniqueIds = Array.from(new Set(updates.map((update) => update.id)));
|
|
359
|
+
const body = uniqueIds.map((id) => ({
|
|
360
|
+
method: "PATCH",
|
|
361
|
+
uri: `/_apis/wit/workitems/${id}?api-version=${batchApiVersion}`,
|
|
362
|
+
headers: {
|
|
363
|
+
"Content-Type": "application/json-patch+json",
|
|
364
|
+
},
|
|
365
|
+
body: updates.filter((update) => update.id === id).map(({ op, path, value }) => ({
|
|
366
|
+
op: op,
|
|
367
|
+
path: path,
|
|
368
|
+
value: value,
|
|
369
|
+
})),
|
|
370
|
+
}));
|
|
371
|
+
const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
|
|
372
|
+
method: "PATCH",
|
|
373
|
+
headers: {
|
|
374
|
+
Authorization: `Bearer ${accessToken.token}`,
|
|
375
|
+
"Content-Type": "application/json",
|
|
376
|
+
"User-Agent": `${userAgent}`,
|
|
377
|
+
},
|
|
378
|
+
body: JSON.stringify(body),
|
|
379
|
+
});
|
|
380
|
+
if (!response.ok) {
|
|
381
|
+
throw new Error(`Failed to update work items in batch: ${response.statusText}`);
|
|
382
|
+
}
|
|
383
|
+
const result = await response.json();
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
server.tool(WORKITEM_TOOLS.work_items_link, "Link work items together in batch.", {
|
|
389
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
390
|
+
updates: z.array(z.object({
|
|
391
|
+
id: z.number().describe("The ID of the work item to update."),
|
|
392
|
+
linkToId: z.number().describe("The ID of the work item to link to."),
|
|
393
|
+
type: z.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests"]).default("related").describe("Type of link to create between the work items. Options include 'parent', 'child', 'duplicate', 'duplicate of', 'related', 'successor', 'predecessor', 'tested by', 'tests', 'referenced by', and 'references'. Defaults to 'related'."),
|
|
394
|
+
comment: z.string().optional().describe("Optional comment to include with the link. This can be used to provide additional context for the link being created."),
|
|
395
|
+
})).describe(""),
|
|
396
|
+
}, async ({ project, updates }) => {
|
|
397
|
+
const connection = await connectionProvider();
|
|
398
|
+
const orgUrl = connection.serverUrl;
|
|
399
|
+
const accessToken = await tokenProvider();
|
|
400
|
+
// Extract unique IDs from the updates array
|
|
401
|
+
const uniqueIds = Array.from(new Set(updates.map((update) => update.id)));
|
|
402
|
+
const body = uniqueIds.map((id) => ({
|
|
403
|
+
method: "PATCH",
|
|
404
|
+
uri: `/_apis/wit/workitems/${id}?api-version=${batchApiVersion}`,
|
|
405
|
+
headers: {
|
|
406
|
+
"Content-Type": "application/json-patch+json",
|
|
407
|
+
},
|
|
408
|
+
body: updates.filter((update) => update.id === id).map(({ linkToId, type, comment }) => ({
|
|
409
|
+
op: "add",
|
|
410
|
+
path: "/relations/-",
|
|
411
|
+
value: {
|
|
412
|
+
rel: `${getLinkTypeFromName(type)}`,
|
|
413
|
+
url: `${orgUrl}/${project}/_apis/wit/workItems/${linkToId}`,
|
|
414
|
+
attributes: {
|
|
415
|
+
comment: comment || "",
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
})),
|
|
419
|
+
}));
|
|
420
|
+
const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
|
|
421
|
+
method: "PATCH",
|
|
422
|
+
headers: {
|
|
423
|
+
Authorization: `Bearer ${accessToken.token}`,
|
|
424
|
+
"Content-Type": "application/json",
|
|
425
|
+
"User-Agent": `${userAgent}`,
|
|
426
|
+
},
|
|
427
|
+
body: JSON.stringify(body),
|
|
428
|
+
});
|
|
429
|
+
if (!response.ok) {
|
|
430
|
+
throw new Error(`Failed to update work items in batch: ${response.statusText}`);
|
|
431
|
+
}
|
|
432
|
+
const result = await response.json();
|
|
433
|
+
return {
|
|
434
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
435
|
+
};
|
|
436
|
+
});
|
|
437
|
+
server.tool(WORKITEM_TOOLS.close_and_link_workitem_duplicates, "Close duplicate work items by id.", {
|
|
438
|
+
id: z.number().describe("The ID of the work item to close and link duplicates to."),
|
|
439
|
+
duplicateIds: z.array(z.number()).describe("An array of IDs of the duplicate work items to close and link to the specified work item."),
|
|
440
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
441
|
+
state: z.string().default("Removed").describe("The state to set for the duplicate work items. Defaults to 'Removed'."),
|
|
442
|
+
}, async ({ id, duplicateIds, project, state }) => {
|
|
443
|
+
const connection = await connectionProvider();
|
|
444
|
+
const body = duplicateIds.map((duplicateId) => ({
|
|
445
|
+
method: "PATCH",
|
|
446
|
+
uri: `/_apis/wit/workitems/${duplicateId}?api-version=${batchApiVersion}`,
|
|
447
|
+
headers: {
|
|
448
|
+
"Content-Type": "application/json-patch+json",
|
|
449
|
+
},
|
|
450
|
+
body: [
|
|
451
|
+
{
|
|
452
|
+
op: "add",
|
|
453
|
+
path: "/fields/System.State",
|
|
454
|
+
value: `${state}`,
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
op: "add",
|
|
458
|
+
path: "/relations/-",
|
|
459
|
+
value: {
|
|
460
|
+
rel: "System.LinkTypes.Duplicate-Reverse",
|
|
461
|
+
url: `${connection.serverUrl}/${project}/_apis/wit/workItems/${id}`,
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
}));
|
|
466
|
+
const accessToken = await tokenProvider();
|
|
467
|
+
const response = await fetch(`${connection.serverUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
|
|
468
|
+
method: "PATCH",
|
|
469
|
+
headers: {
|
|
470
|
+
Authorization: `Bearer ${accessToken.token}`,
|
|
471
|
+
"Content-Type": "application/json",
|
|
472
|
+
"User-Agent": `${userAgent}`,
|
|
473
|
+
},
|
|
474
|
+
body: JSON.stringify(body),
|
|
475
|
+
});
|
|
476
|
+
if (!response.ok) {
|
|
477
|
+
throw new Error(`Failed to update work items in batch: ${response.statusText}`);
|
|
478
|
+
}
|
|
479
|
+
const result = await response.json();
|
|
480
|
+
return {
|
|
481
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
export { WORKITEM_TOOLS, configureWorkItemTools };
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { configureCoreTools } from "./tools/core.js";
|
|
4
|
+
import { configureBuildTools } from "./tools/builds.js";
|
|
5
|
+
import { configureRepoTools } from "./tools/repos.js";
|
|
6
|
+
import { configureWorkItemTools } from "./tools/workitems.js";
|
|
7
|
+
import { configureReleaseTools } from "./tools/releases.js";
|
|
8
|
+
import { configureWikiTools } from "./tools/wiki.js";
|
|
9
|
+
import { configureTestPlanTools } from "./tools/testplans.js";
|
|
10
|
+
import { configureSearchTools } from "./tools/search.js";
|
|
11
|
+
function configureAllTools(server, tokenProvider, connectionProvider) {
|
|
12
|
+
configureCoreTools(server, tokenProvider, connectionProvider);
|
|
13
|
+
configureBuildTools(server, tokenProvider, connectionProvider);
|
|
14
|
+
configureRepoTools(server, tokenProvider, connectionProvider);
|
|
15
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
16
|
+
configureReleaseTools(server, tokenProvider, connectionProvider);
|
|
17
|
+
configureWikiTools(server, tokenProvider, connectionProvider);
|
|
18
|
+
configureTestPlanTools(server, tokenProvider, connectionProvider);
|
|
19
|
+
configureSearchTools(server, tokenProvider, connectionProvider);
|
|
20
|
+
}
|
|
21
|
+
export { configureAllTools };
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { packageVersion } from "./version.js";
|
|
4
|
+
export const apiVersion = "7.2-preview.1";
|
|
5
|
+
export const batchApiVersion = "5.0";
|
|
6
|
+
export const userAgent = `AzureDevOps.MCP/${packageVersion} (local)`;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const packageVersion = "0.1.0";
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@azure-devops/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for interacting with Azure DevOps",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Microsoft Corporation",
|
|
7
|
+
"homepage": "TBD",
|
|
8
|
+
"bugs": "TBD",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"bin": {
|
|
11
|
+
"mcp-server-azuredevops": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"preinstall": "npm config set registry https://registry.npmjs.org/",
|
|
21
|
+
"prebuild": "node -p \"'export const packageVersion = ' + JSON.stringify(require('./package.json').version) + ';\\r'\" > src/version.ts",
|
|
22
|
+
"build": "tsc && shx chmod +x dist/*.js",
|
|
23
|
+
"prepare": "npm run build",
|
|
24
|
+
"watch": "tsc --watch",
|
|
25
|
+
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
26
|
+
"start": "node -r tsconfig-paths/register dist/index.js",
|
|
27
|
+
"eslint": "eslint",
|
|
28
|
+
"eslint-fix": "eslint --fix",
|
|
29
|
+
"clean": "shx rm -rf dist",
|
|
30
|
+
"test": "jest"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@azure/identity": "^4.10.0",
|
|
34
|
+
"@modelcontextprotocol/sdk": "1.12.1",
|
|
35
|
+
"azure-devops-extension-api": "^4.252.0",
|
|
36
|
+
"azure-devops-extension-sdk": "^4.0.2",
|
|
37
|
+
"azure-devops-node-api": "^15.1.0",
|
|
38
|
+
"save-dev": "^0.0.1-security",
|
|
39
|
+
"zod": "^3.25.55",
|
|
40
|
+
"zod-to-json-schema": "^3.24.5"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@modelcontextprotocol/inspector": "^0.14.0",
|
|
44
|
+
"@types/node": "^22",
|
|
45
|
+
"@types/jest": "^29.5.14",
|
|
46
|
+
"eslint-plugin-header": "^3.1.1",
|
|
47
|
+
"jest": "^29.7.0",
|
|
48
|
+
"jest-extended": "^6.0.0",
|
|
49
|
+
"shx": "^0.4.0",
|
|
50
|
+
"ts-jest": "^29.3.4",
|
|
51
|
+
"tsconfig-paths": "^4.2.0",
|
|
52
|
+
"typescript": "^5.8.3",
|
|
53
|
+
"typescript-eslint": "^8.32.1"
|
|
54
|
+
}
|
|
55
|
+
}
|