@azure-devops/mcp 2.5.0 → 2.6.0-nightly.20260418
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/README.md +0 -12
- package/dist/auth.js +13 -0
- package/dist/index.js +23 -5
- package/dist/logger.js +0 -0
- package/dist/org-tenants.js +0 -0
- package/dist/prompts.js +0 -0
- package/dist/shared/content-safety.js +24 -0
- package/dist/shared/domains.js +7 -2
- package/dist/tools/advanced-security.js +2 -2
- package/dist/tools/core.js +5 -5
- package/dist/tools/mcp-apps.js +22 -0
- package/dist/tools/pipelines.js +33 -26
- package/dist/tools/repositories.js +536 -85
- package/dist/tools/search.js +10 -7
- package/dist/tools/test-plans.js +109 -25
- package/dist/tools/wiki.js +7 -6
- package/dist/tools/work-items.js +330 -90
- package/dist/tools/work.js +2 -2
- package/dist/tools.js +3 -1
- package/dist/useragent.js +0 -0
- package/dist/utils.js +15 -0
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/dist/tools/work-items.js
CHANGED
|
@@ -4,6 +4,8 @@ import { WorkItemExpand } from "azure-devops-node-api/interfaces/WorkItemTrackin
|
|
|
4
4
|
import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { batchApiVersion, markdownCommentsApiVersion, getEnumKeys, safeEnumConvert, encodeFormattedValue } from "../utils.js";
|
|
7
|
+
import { elicitProject, elicitTeam } from "../shared/elicitations.js";
|
|
8
|
+
import { createExternalContentResponse } from "../shared/content-safety.js";
|
|
7
9
|
const WORKITEM_TOOLS = {
|
|
8
10
|
my_work_items: "wit_my_work_items",
|
|
9
11
|
list_backlogs: "wit_list_backlogs",
|
|
@@ -26,6 +28,8 @@ const WORKITEM_TOOLS = {
|
|
|
26
28
|
work_items_link: "wit_work_items_link",
|
|
27
29
|
work_item_unlink: "wit_work_item_unlink",
|
|
28
30
|
add_artifact_link: "wit_add_artifact_link",
|
|
31
|
+
get_work_item_attachment: "wit_get_work_item_attachment",
|
|
32
|
+
query_by_wiql: "wit_query_by_wiql",
|
|
29
33
|
};
|
|
30
34
|
function getLinkTypeFromName(name) {
|
|
31
35
|
switch (name.toLowerCase()) {
|
|
@@ -58,14 +62,28 @@ function getLinkTypeFromName(name) {
|
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
function configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
61
|
-
server.tool(WORKITEM_TOOLS.list_backlogs, "Receive a list of backlogs for a given project and team.", {
|
|
62
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
63
|
-
team: z.string().describe("The name or ID of the Azure DevOps team."),
|
|
65
|
+
server.tool(WORKITEM_TOOLS.list_backlogs, "Receive a list of backlogs for a given project and team. If a project or team is not specified, you will be prompted to select one.", {
|
|
66
|
+
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."),
|
|
67
|
+
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."),
|
|
64
68
|
}, async ({ project, team }) => {
|
|
65
69
|
try {
|
|
66
70
|
const connection = await connectionProvider();
|
|
71
|
+
let resolvedProject = project;
|
|
72
|
+
if (!resolvedProject) {
|
|
73
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to list backlogs for.");
|
|
74
|
+
if ("response" in result)
|
|
75
|
+
return result.response;
|
|
76
|
+
resolvedProject = result.resolved;
|
|
77
|
+
}
|
|
78
|
+
let resolvedTeam = team;
|
|
79
|
+
if (!resolvedTeam) {
|
|
80
|
+
const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to list backlogs for.");
|
|
81
|
+
if ("response" in result)
|
|
82
|
+
return result.response;
|
|
83
|
+
resolvedTeam = result.resolved;
|
|
84
|
+
}
|
|
67
85
|
const workApi = await connection.getWorkApi();
|
|
68
|
-
const teamContext = { project, team };
|
|
86
|
+
const teamContext = { project: resolvedProject, team: resolvedTeam };
|
|
69
87
|
const backlogs = await workApi.getBacklogs(teamContext);
|
|
70
88
|
return {
|
|
71
89
|
content: [{ type: "text", text: JSON.stringify(backlogs, null, 2) }],
|
|
@@ -79,15 +97,29 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
79
97
|
};
|
|
80
98
|
}
|
|
81
99
|
});
|
|
82
|
-
server.tool(WORKITEM_TOOLS.list_backlog_work_items, "Retrieve a list of backlogs of for a given project, team, and backlog category", {
|
|
83
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
84
|
-
team: z.string().describe("The name or ID of the Azure DevOps team."),
|
|
100
|
+
server.tool(WORKITEM_TOOLS.list_backlog_work_items, "Retrieve a list of backlogs of for a given project, team, and backlog category. If a project or team 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
|
+
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."),
|
|
85
103
|
backlogId: z.string().describe("The ID of the backlog category to retrieve work items from."),
|
|
86
104
|
}, async ({ project, team, backlogId }) => {
|
|
87
105
|
try {
|
|
88
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 backlog work items for.");
|
|
110
|
+
if ("response" in result)
|
|
111
|
+
return result.response;
|
|
112
|
+
resolvedProject = result.resolved;
|
|
113
|
+
}
|
|
114
|
+
let resolvedTeam = team;
|
|
115
|
+
if (!resolvedTeam) {
|
|
116
|
+
const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to list backlog work items for.");
|
|
117
|
+
if ("response" in result)
|
|
118
|
+
return result.response;
|
|
119
|
+
resolvedTeam = result.resolved;
|
|
120
|
+
}
|
|
89
121
|
const workApi = await connection.getWorkApi();
|
|
90
|
-
const teamContext = { project, team };
|
|
122
|
+
const teamContext = { project: resolvedProject, team: resolvedTeam };
|
|
91
123
|
const workItems = await workApi.getBacklogLevelWorkItems(teamContext, backlogId);
|
|
92
124
|
return {
|
|
93
125
|
content: [{ type: "text", text: JSON.stringify(workItems, null, 2) }],
|
|
@@ -101,16 +133,23 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
101
133
|
};
|
|
102
134
|
}
|
|
103
135
|
});
|
|
104
|
-
server.tool(WORKITEM_TOOLS.my_work_items, "Retrieve a list of work items relevent to the authenticated user.", {
|
|
105
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
136
|
+
server.tool(WORKITEM_TOOLS.my_work_items, "Retrieve a list of work items relevent to the authenticated user. If a project is not specified, you will be prompted to select one.", {
|
|
137
|
+
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."),
|
|
106
138
|
type: z.enum(["assignedtome", "myactivity"]).default("assignedtome").describe("The type of work items to retrieve. Defaults to 'assignedtome'."),
|
|
107
|
-
top: z.number().default(50).describe("The maximum number of work items to return. Defaults to 50."),
|
|
139
|
+
top: z.coerce.number().default(50).describe("The maximum number of work items to return. Defaults to 50."),
|
|
108
140
|
includeCompleted: z.boolean().default(false).describe("Whether to include completed work items. Defaults to false."),
|
|
109
141
|
}, async ({ project, type, top, includeCompleted }) => {
|
|
110
142
|
try {
|
|
111
143
|
const connection = await connectionProvider();
|
|
144
|
+
let resolvedProject = project;
|
|
145
|
+
if (!resolvedProject) {
|
|
146
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve work items for.");
|
|
147
|
+
if ("response" in result)
|
|
148
|
+
return result.response;
|
|
149
|
+
resolvedProject = result.resolved;
|
|
150
|
+
}
|
|
112
151
|
const workApi = await connection.getWorkApi();
|
|
113
|
-
const workItems = await workApi.getPredefinedQueryResults(
|
|
152
|
+
const workItems = await workApi.getPredefinedQueryResults(resolvedProject, type, top, includeCompleted);
|
|
114
153
|
return {
|
|
115
154
|
content: [{ type: "text", text: JSON.stringify(workItems, null, 2) }],
|
|
116
155
|
};
|
|
@@ -123,18 +162,25 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
123
162
|
};
|
|
124
163
|
}
|
|
125
164
|
});
|
|
126
|
-
server.tool(WORKITEM_TOOLS.get_work_items_batch_by_ids, "Retrieve list of work items by IDs in batch.", {
|
|
127
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
128
|
-
ids: z.array(z.number()).describe("The IDs of the work items to retrieve."),
|
|
165
|
+
server.tool(WORKITEM_TOOLS.get_work_items_batch_by_ids, "Retrieve list of work items by IDs in batch. If a project is not specified, you will be prompted to select one.", {
|
|
166
|
+
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."),
|
|
167
|
+
ids: z.array(z.coerce.number().min(1)).describe("The IDs of the work items to retrieve."),
|
|
129
168
|
fields: z.array(z.string()).optional().describe("Optional list of fields to include in the response. If not provided, a hardcoded default set of fields will be used."),
|
|
130
169
|
}, async ({ project, ids, fields }) => {
|
|
131
170
|
try {
|
|
132
171
|
const connection = await connectionProvider();
|
|
172
|
+
let resolvedProject = project;
|
|
173
|
+
if (!resolvedProject) {
|
|
174
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve work items for.");
|
|
175
|
+
if ("response" in result)
|
|
176
|
+
return result.response;
|
|
177
|
+
resolvedProject = result.resolved;
|
|
178
|
+
}
|
|
133
179
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
134
180
|
const defaultFields = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank", "System.AssignedTo"];
|
|
135
181
|
// If no fields are provided, use the default set of fields
|
|
136
182
|
const fieldsToUse = !fields || fields.length === 0 ? defaultFields : fields;
|
|
137
|
-
const workitems = await workItemApi.getWorkItemsBatch({ ids, fields: fieldsToUse },
|
|
183
|
+
const workitems = await workItemApi.getWorkItemsBatch({ ids, fields: fieldsToUse }, resolvedProject);
|
|
138
184
|
// List of identity fields that need to be transformed from objects to formatted strings
|
|
139
185
|
const identityFields = [
|
|
140
186
|
"System.AssignedTo",
|
|
@@ -173,21 +219,36 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
173
219
|
};
|
|
174
220
|
}
|
|
175
221
|
});
|
|
176
|
-
server.tool(WORKITEM_TOOLS.get_work_item, "Get a single work item by ID.", {
|
|
177
|
-
id: z.number().describe("The ID of the work item to retrieve."),
|
|
178
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
179
|
-
fields: z
|
|
222
|
+
server.tool(WORKITEM_TOOLS.get_work_item, "Get a single work item by ID. If a project is not specified, you will be prompted to select one.", {
|
|
223
|
+
id: z.coerce.number().min(1).describe("The ID of the work item to retrieve."),
|
|
224
|
+
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."),
|
|
225
|
+
fields: z
|
|
226
|
+
.array(z.string())
|
|
227
|
+
.optional()
|
|
228
|
+
.describe("Optional list of fields to include in the response. If not provided, all fields will be returned. Cannot be used together with the expand parameter."),
|
|
180
229
|
asOf: z.coerce.date().optional().describe("Optional date string to retrieve the work item as of a specific time. If not provided, the current state will be returned."),
|
|
181
230
|
expand: z
|
|
182
231
|
.enum(["all", "fields", "links", "none", "relations"])
|
|
183
|
-
.describe("Optional expand parameter to include additional details in the response.")
|
|
232
|
+
.describe("Optional expand parameter to include additional details in the response. Cannot be used together with the fields parameter.")
|
|
184
233
|
.optional()
|
|
185
|
-
.describe("Expand options include '
|
|
234
|
+
.describe("Expand options include 'All', 'Fields', 'Links', 'None', and 'Relations'. Relations can be used to get child workitems. Defaults to 'None'. Cannot be used together with the fields parameter."),
|
|
186
235
|
}, async ({ id, project, fields, asOf, expand }) => {
|
|
187
236
|
try {
|
|
188
237
|
const connection = await connectionProvider();
|
|
238
|
+
let resolvedProject = project;
|
|
239
|
+
if (!resolvedProject) {
|
|
240
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the work item from.");
|
|
241
|
+
if ("response" in result)
|
|
242
|
+
return result.response;
|
|
243
|
+
resolvedProject = result.resolved;
|
|
244
|
+
}
|
|
245
|
+
// The Azure DevOps API does not support using expand and fields together.
|
|
246
|
+
// When both are provided, prefer fields as it is the more specific selection.
|
|
247
|
+
if (fields && fields.length > 0 && expand != null) {
|
|
248
|
+
expand = "none";
|
|
249
|
+
}
|
|
189
250
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
190
|
-
const workItem = await workItemApi.getWorkItem(id, fields, asOf, expand,
|
|
251
|
+
const workItem = await workItemApi.getWorkItem(id, fields, asOf, expand, resolvedProject);
|
|
191
252
|
return {
|
|
192
253
|
content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
|
|
193
254
|
};
|
|
@@ -200,15 +261,22 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
200
261
|
};
|
|
201
262
|
}
|
|
202
263
|
});
|
|
203
|
-
server.tool(WORKITEM_TOOLS.list_work_item_comments, "Retrieve list of comments for a work item by ID.", {
|
|
204
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
205
|
-
workItemId: z.number().describe("The ID of the work item to retrieve comments for."),
|
|
206
|
-
top: z.number().default(50).describe("Optional number of comments to retrieve. Defaults to all comments."),
|
|
264
|
+
server.tool(WORKITEM_TOOLS.list_work_item_comments, "Retrieve list of comments for a work item by ID. If a project is not specified, you will be prompted to select one.", {
|
|
265
|
+
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."),
|
|
266
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item to retrieve comments for."),
|
|
267
|
+
top: z.coerce.number().default(50).describe("Optional number of comments to retrieve. Defaults to all comments."),
|
|
207
268
|
}, async ({ project, workItemId, top }) => {
|
|
208
269
|
try {
|
|
209
270
|
const connection = await connectionProvider();
|
|
271
|
+
let resolvedProject = project;
|
|
272
|
+
if (!resolvedProject) {
|
|
273
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to list work item comments for.");
|
|
274
|
+
if ("response" in result)
|
|
275
|
+
return result.response;
|
|
276
|
+
resolvedProject = result.resolved;
|
|
277
|
+
}
|
|
210
278
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
211
|
-
const comments = await workItemApi.getComments(
|
|
279
|
+
const comments = await workItemApi.getComments(resolvedProject, workItemId, top);
|
|
212
280
|
return {
|
|
213
281
|
content: [{ type: "text", text: JSON.stringify(comments, null, 2) }],
|
|
214
282
|
};
|
|
@@ -221,21 +289,28 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
221
289
|
};
|
|
222
290
|
}
|
|
223
291
|
});
|
|
224
|
-
server.tool(WORKITEM_TOOLS.add_work_item_comment, "Add comment to a work item by ID.", {
|
|
225
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
226
|
-
workItemId: z.number().describe("The ID of the work item to add a comment to."),
|
|
292
|
+
server.tool(WORKITEM_TOOLS.add_work_item_comment, "Add comment to a work item by ID. If a project is not specified, you will be prompted to select one.", {
|
|
293
|
+
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."),
|
|
294
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item to add a comment to."),
|
|
227
295
|
comment: z.string().describe("The text of the comment to add to the work item."),
|
|
228
|
-
format: z.enum(["
|
|
296
|
+
format: z.enum(["Markdown", "Html"]).optional().default("Markdown").describe("The format of the comment text, e.g., 'Markdown', 'Html'. Optional, defaults to 'Markdown'."),
|
|
229
297
|
}, async ({ project, workItemId, comment, format }) => {
|
|
230
298
|
try {
|
|
231
299
|
const connection = await connectionProvider();
|
|
300
|
+
let resolvedProject = project;
|
|
301
|
+
if (!resolvedProject) {
|
|
302
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to add a work item comment in.");
|
|
303
|
+
if ("response" in result)
|
|
304
|
+
return result.response;
|
|
305
|
+
resolvedProject = result.resolved;
|
|
306
|
+
}
|
|
232
307
|
const orgUrl = connection.serverUrl;
|
|
233
308
|
const accessToken = await tokenProvider();
|
|
234
309
|
const body = {
|
|
235
310
|
text: comment,
|
|
236
311
|
};
|
|
237
|
-
const formatParameter = format === "
|
|
238
|
-
const response = await fetch(`${orgUrl}/${
|
|
312
|
+
const formatParameter = (format ?? "Markdown") === "Markdown" ? 0 : 1;
|
|
313
|
+
const response = await fetch(`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
|
|
239
314
|
method: "POST",
|
|
240
315
|
headers: {
|
|
241
316
|
"Authorization": `Bearer ${accessToken}`,
|
|
@@ -260,20 +335,27 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
260
335
|
};
|
|
261
336
|
}
|
|
262
337
|
});
|
|
263
|
-
server.tool(WORKITEM_TOOLS.update_work_item_comment, "Update an existing comment on a work item by ID.", {
|
|
264
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
265
|
-
workItemId: z.number().describe("The ID of the work item."),
|
|
266
|
-
commentId: z.number().describe("The ID of the comment to update."),
|
|
338
|
+
server.tool(WORKITEM_TOOLS.update_work_item_comment, "Update an existing comment on a work item by ID. If a project is not specified, you will be prompted to select one.", {
|
|
339
|
+
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."),
|
|
340
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item."),
|
|
341
|
+
commentId: z.coerce.number().min(1).describe("The ID of the comment to update."),
|
|
267
342
|
text: z.string().describe("The updated comment text."),
|
|
268
|
-
format: z.enum(["
|
|
343
|
+
format: z.enum(["Markdown", "Html"]).optional().default("Markdown").describe("The format of the comment text, e.g., 'Markdown', 'Html'. Optional, defaults to 'Markdown'."),
|
|
269
344
|
}, async ({ project, workItemId, commentId, text, format }) => {
|
|
270
345
|
try {
|
|
271
346
|
const connection = await connectionProvider();
|
|
347
|
+
let resolvedProject = project;
|
|
348
|
+
if (!resolvedProject) {
|
|
349
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to update the work item comment in.");
|
|
350
|
+
if ("response" in result)
|
|
351
|
+
return result.response;
|
|
352
|
+
resolvedProject = result.resolved;
|
|
353
|
+
}
|
|
272
354
|
const orgUrl = connection.serverUrl;
|
|
273
355
|
const accessToken = await tokenProvider();
|
|
274
356
|
const body = { text };
|
|
275
|
-
const formatParameter = format === "
|
|
276
|
-
const response = await fetch(`${orgUrl}/${
|
|
357
|
+
const formatParameter = (format ?? "Markdown") === "Markdown" ? 0 : 1;
|
|
358
|
+
const response = await fetch(`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments/${commentId}?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
|
|
277
359
|
method: "PATCH",
|
|
278
360
|
headers: {
|
|
279
361
|
"Authorization": `Bearer ${accessToken}`,
|
|
@@ -298,11 +380,11 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
298
380
|
};
|
|
299
381
|
}
|
|
300
382
|
});
|
|
301
|
-
server.tool(WORKITEM_TOOLS.list_work_item_revisions, "Retrieve list of revisions for a work item by ID.", {
|
|
302
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
303
|
-
workItemId: z.number().describe("The ID of the work item to retrieve revisions for."),
|
|
304
|
-
top: z.number().default(50).describe("Optional number of revisions to retrieve. If not provided, all revisions will be returned."),
|
|
305
|
-
skip: z.number().optional().describe("Optional number of revisions to skip for pagination. Defaults to 0."),
|
|
383
|
+
server.tool(WORKITEM_TOOLS.list_work_item_revisions, "Retrieve list of revisions for a work item by ID. If a project is not specified, you will be prompted to select one.", {
|
|
384
|
+
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."),
|
|
385
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item to retrieve revisions for."),
|
|
386
|
+
top: z.coerce.number().default(50).describe("Optional number of revisions to retrieve. If not provided, all revisions will be returned."),
|
|
387
|
+
skip: z.coerce.number().optional().describe("Optional number of revisions to skip for pagination. Defaults to 0."),
|
|
306
388
|
expand: z
|
|
307
389
|
.enum(getEnumKeys(WorkItemExpand))
|
|
308
390
|
.default("None")
|
|
@@ -311,8 +393,15 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
311
393
|
}, async ({ project, workItemId, top, skip, expand }) => {
|
|
312
394
|
try {
|
|
313
395
|
const connection = await connectionProvider();
|
|
396
|
+
let resolvedProject = project;
|
|
397
|
+
if (!resolvedProject) {
|
|
398
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to list work item revisions for.");
|
|
399
|
+
if ("response" in result)
|
|
400
|
+
return result.response;
|
|
401
|
+
resolvedProject = result.resolved;
|
|
402
|
+
}
|
|
314
403
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
315
|
-
const revisions = await workItemApi.getRevisions(workItemId, top, skip, safeEnumConvert(WorkItemExpand, expand),
|
|
404
|
+
const revisions = await workItemApi.getRevisions(workItemId, top, skip, safeEnumConvert(WorkItemExpand, expand), resolvedProject);
|
|
316
405
|
// Dynamically clean up identity objects in revision fields
|
|
317
406
|
// Identity objects typically have properties like displayName, url, _links, id, uniqueName, imageUrl, descriptor
|
|
318
407
|
if (revisions && Array.isArray(revisions)) {
|
|
@@ -350,20 +439,27 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
350
439
|
};
|
|
351
440
|
}
|
|
352
441
|
});
|
|
353
|
-
server.tool(WORKITEM_TOOLS.add_child_work_items, "Create one or many child work items from a parent by work item type and parent id.", {
|
|
354
|
-
parentId: z.number().describe("The ID of the parent work item to create a child work item under."),
|
|
355
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
442
|
+
server.tool(WORKITEM_TOOLS.add_child_work_items, "Create one or many child work items from a parent by work item type and parent id. If a project is not specified, you will be prompted to select one.", {
|
|
443
|
+
parentId: z.coerce.number().min(1).describe("The ID of the parent work item to create a child work item under."),
|
|
444
|
+
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."),
|
|
356
445
|
workItemType: z.string().describe("The type of the child work item to create."),
|
|
357
446
|
items: z.array(z.object({
|
|
358
447
|
title: z.string().describe("The title of the child work item."),
|
|
359
448
|
description: z.string().describe("The description of the child work item."),
|
|
360
|
-
format: z.enum(["Markdown", "Html"]).default("
|
|
449
|
+
format: z.enum(["Markdown", "Html"]).default("Markdown").describe("Format for the description on the child work item, e.g., 'Markdown', 'Html'. Defaults to 'Markdown'."),
|
|
361
450
|
areaPath: z.string().optional().describe("Optional area path for the child work item."),
|
|
362
451
|
iterationPath: z.string().optional().describe("Optional iteration path for the child work item."),
|
|
363
452
|
})),
|
|
364
453
|
}, async ({ parentId, project, workItemType, items }) => {
|
|
365
454
|
try {
|
|
366
455
|
const connection = await connectionProvider();
|
|
456
|
+
let resolvedProject = project;
|
|
457
|
+
if (!resolvedProject) {
|
|
458
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to create child work items in.");
|
|
459
|
+
if ("response" in result)
|
|
460
|
+
return result.response;
|
|
461
|
+
resolvedProject = result.resolved;
|
|
462
|
+
}
|
|
367
463
|
const orgUrl = connection.serverUrl;
|
|
368
464
|
const accessToken = await tokenProvider();
|
|
369
465
|
if (items.length > 50) {
|
|
@@ -400,7 +496,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
400
496
|
path: "/relations/-",
|
|
401
497
|
value: {
|
|
402
498
|
rel: "System.LinkTypes.Hierarchy-Reverse",
|
|
403
|
-
url: `${connection.serverUrl}/${
|
|
499
|
+
url: `${connection.serverUrl}/${resolvedProject}/_apis/wit/workItems/${parentId}`,
|
|
404
500
|
},
|
|
405
501
|
},
|
|
406
502
|
];
|
|
@@ -432,7 +528,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
432
528
|
}
|
|
433
529
|
return {
|
|
434
530
|
method: "PATCH",
|
|
435
|
-
uri: `/${
|
|
531
|
+
uri: `/${encodeURIComponent(resolvedProject)}/_apis/wit/workitems/$${encodeURIComponent(workItemType)}?api-version=${batchApiVersion}`,
|
|
436
532
|
headers: {
|
|
437
533
|
"Content-Type": "application/json-patch+json",
|
|
438
534
|
},
|
|
@@ -467,8 +563,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
467
563
|
server.tool(WORKITEM_TOOLS.link_work_item_to_pull_request, "Link a single work item to an existing pull request.", {
|
|
468
564
|
projectId: z.string().describe("The project ID of the Azure DevOps project (note: project name is not valid)."),
|
|
469
565
|
repositoryId: z.string().describe("The ID of the repository containing the pull request. Do not use the repository name here, use the ID instead."),
|
|
470
|
-
pullRequestId: z.number().describe("The ID of the pull request to link to."),
|
|
471
|
-
workItemId: z.number().describe("The ID of the work item to link to the pull request."),
|
|
566
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request to link to."),
|
|
567
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item to link to the pull request."),
|
|
472
568
|
pullRequestProjectId: z.string().optional().describe("The project ID containing the pull request. If not provided, defaults to the work item's project ID (for same-project linking)."),
|
|
473
569
|
}, async ({ projectId, repositoryId, pullRequestId, workItemId, pullRequestProjectId }) => {
|
|
474
570
|
try {
|
|
@@ -519,16 +615,23 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
519
615
|
};
|
|
520
616
|
}
|
|
521
617
|
});
|
|
522
|
-
server.tool(WORKITEM_TOOLS.get_work_items_for_iteration, "Retrieve a list of work items for a specified iteration.", {
|
|
523
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
618
|
+
server.tool(WORKITEM_TOOLS.get_work_items_for_iteration, "Retrieve a list of work items for a specified iteration. If a project is not specified, you will be prompted to select one.", {
|
|
619
|
+
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."),
|
|
524
620
|
team: z.string().optional().describe("The name or ID of the Azure DevOps team. If not provided, the default team will be used."),
|
|
525
621
|
iterationId: z.string().describe("The ID of the iteration to retrieve work items for."),
|
|
526
622
|
}, async ({ project, team, iterationId }) => {
|
|
527
623
|
try {
|
|
528
624
|
const connection = await connectionProvider();
|
|
625
|
+
let resolvedProject = project;
|
|
626
|
+
if (!resolvedProject) {
|
|
627
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve work items for iteration.");
|
|
628
|
+
if ("response" in result)
|
|
629
|
+
return result.response;
|
|
630
|
+
resolvedProject = result.resolved;
|
|
631
|
+
}
|
|
529
632
|
const workApi = await connection.getWorkApi();
|
|
530
633
|
//get the work items for the current iteration
|
|
531
|
-
const workItems = await workApi.getIterationWorkItems({ project, team }, iterationId);
|
|
634
|
+
const workItems = await workApi.getIterationWorkItems({ project: resolvedProject, team }, iterationId);
|
|
532
635
|
return {
|
|
533
636
|
content: [{ type: "text", text: JSON.stringify(workItems, null, 2) }],
|
|
534
637
|
};
|
|
@@ -542,7 +645,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
542
645
|
}
|
|
543
646
|
});
|
|
544
647
|
server.tool(WORKITEM_TOOLS.update_work_item, "Update a work item by ID with specified fields.", {
|
|
545
|
-
id: z.number().describe("The ID of the work item to update."),
|
|
648
|
+
id: z.coerce.number().min(1).describe("The ID of the work item to update."),
|
|
546
649
|
updates: z
|
|
547
650
|
.array(z.object({
|
|
548
651
|
op: z
|
|
@@ -577,14 +680,21 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
577
680
|
};
|
|
578
681
|
}
|
|
579
682
|
});
|
|
580
|
-
server.tool(WORKITEM_TOOLS.get_work_item_type, "Get a specific work item type.", {
|
|
581
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
683
|
+
server.tool(WORKITEM_TOOLS.get_work_item_type, "Get a specific work item type. If a project is not specified, you will be prompted to select one.", {
|
|
684
|
+
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."),
|
|
582
685
|
workItemType: z.string().describe("The name of the work item type to retrieve."),
|
|
583
686
|
}, async ({ project, workItemType }) => {
|
|
584
687
|
try {
|
|
585
688
|
const connection = await connectionProvider();
|
|
689
|
+
let resolvedProject = project;
|
|
690
|
+
if (!resolvedProject) {
|
|
691
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the work item type from.");
|
|
692
|
+
if ("response" in result)
|
|
693
|
+
return result.response;
|
|
694
|
+
resolvedProject = result.resolved;
|
|
695
|
+
}
|
|
586
696
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
587
|
-
const workItemTypeInfo = await workItemApi.getWorkItemType(
|
|
697
|
+
const workItemTypeInfo = await workItemApi.getWorkItemType(resolvedProject, workItemType);
|
|
588
698
|
return {
|
|
589
699
|
content: [{ type: "text", text: JSON.stringify(workItemTypeInfo, null, 2) }],
|
|
590
700
|
};
|
|
@@ -597,19 +707,26 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
597
707
|
};
|
|
598
708
|
}
|
|
599
709
|
});
|
|
600
|
-
server.tool(WORKITEM_TOOLS.create_work_item, "Create a new work item in a specified project and work item type.", {
|
|
601
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
710
|
+
server.tool(WORKITEM_TOOLS.create_work_item, "Create a new work item in a specified project and work item type. If a project is not specified, you will be prompted to select one.", {
|
|
711
|
+
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."),
|
|
602
712
|
workItemType: z.string().describe("The type of work item to create, e.g., 'Task', 'Bug', etc."),
|
|
603
713
|
fields: z
|
|
604
714
|
.array(z.object({
|
|
605
715
|
name: z.string().describe("The name of the field, e.g., 'System.Title'."),
|
|
606
716
|
value: z.string().describe("The value of the field."),
|
|
607
|
-
format: z.enum(["Html", "Markdown"]).optional().describe("the format of the field value, e.g., 'Html', 'Markdown'. Optional, defaults to '
|
|
717
|
+
format: z.enum(["Html", "Markdown"]).optional().default("Markdown").describe("the format of the field value, e.g., 'Html', 'Markdown'. Optional, defaults to 'Markdown'."),
|
|
608
718
|
}))
|
|
609
719
|
.describe("A record of field names and values to set on the new work item. Each fild is the field name and each value is the corresponding value to set for that field."),
|
|
610
720
|
}, async ({ project, workItemType, fields }) => {
|
|
611
721
|
try {
|
|
612
722
|
const connection = await connectionProvider();
|
|
723
|
+
let resolvedProject = project;
|
|
724
|
+
if (!resolvedProject) {
|
|
725
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to create the work item in.");
|
|
726
|
+
if ("response" in result)
|
|
727
|
+
return result.response;
|
|
728
|
+
resolvedProject = result.resolved;
|
|
729
|
+
}
|
|
613
730
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
614
731
|
const document = fields.map(({ name, value, format }) => ({
|
|
615
732
|
op: "add",
|
|
@@ -628,7 +745,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
628
745
|
});
|
|
629
746
|
}
|
|
630
747
|
});
|
|
631
|
-
const newWorkItem = await workItemApi.createWorkItem(null, document,
|
|
748
|
+
const newWorkItem = await workItemApi.createWorkItem(null, document, resolvedProject, workItemType);
|
|
632
749
|
if (!newWorkItem) {
|
|
633
750
|
return { content: [{ type: "text", text: "Work item was not created" }], isError: true };
|
|
634
751
|
}
|
|
@@ -644,21 +761,28 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
644
761
|
};
|
|
645
762
|
}
|
|
646
763
|
});
|
|
647
|
-
server.tool(WORKITEM_TOOLS.get_query, "Get a query by its ID or path.", {
|
|
648
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
764
|
+
server.tool(WORKITEM_TOOLS.get_query, "Get a query by its ID or path. If a project is not specified, you will be prompted to select one.", {
|
|
765
|
+
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."),
|
|
649
766
|
query: z.string().describe("The ID or path of the query to retrieve."),
|
|
650
767
|
expand: z
|
|
651
768
|
.enum(getEnumKeys(QueryExpand))
|
|
652
769
|
.optional()
|
|
653
770
|
.describe("Optional expand parameter to include additional details in the response. Defaults to 'None'."),
|
|
654
|
-
depth: z.number().default(0).describe("Optional depth parameter to specify how deep to expand the query. Defaults to 0."),
|
|
771
|
+
depth: z.coerce.number().default(0).describe("Optional depth parameter to specify how deep to expand the query. Defaults to 0."),
|
|
655
772
|
includeDeleted: z.boolean().default(false).describe("Whether to include deleted items in the query results. Defaults to false."),
|
|
656
773
|
useIsoDateFormat: z.boolean().default(false).describe("Whether to use ISO date format in the response. Defaults to false."),
|
|
657
774
|
}, async ({ project, query, expand, depth, includeDeleted, useIsoDateFormat }) => {
|
|
658
775
|
try {
|
|
659
776
|
const connection = await connectionProvider();
|
|
777
|
+
let resolvedProject = project;
|
|
778
|
+
if (!resolvedProject) {
|
|
779
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the query from.");
|
|
780
|
+
if ("response" in result)
|
|
781
|
+
return result.response;
|
|
782
|
+
resolvedProject = result.resolved;
|
|
783
|
+
}
|
|
660
784
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
661
|
-
const queryDetails = await workItemApi.getQuery(
|
|
785
|
+
const queryDetails = await workItemApi.getQuery(resolvedProject, query, safeEnumConvert(QueryExpand, expand), depth, includeDeleted, useIsoDateFormat);
|
|
662
786
|
return {
|
|
663
787
|
content: [{ type: "text", text: JSON.stringify(queryDetails, null, 2) }],
|
|
664
788
|
};
|
|
@@ -676,7 +800,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
676
800
|
project: z.string().optional().describe("The name or ID of the Azure DevOps project. If not provided, the default project will be used."),
|
|
677
801
|
team: z.string().optional().describe("The name or ID of the Azure DevOps team. If not provided, the default team will be used."),
|
|
678
802
|
timePrecision: z.boolean().optional().describe("Whether to include time precision in the results. Defaults to false."),
|
|
679
|
-
top: z.number().default(50).describe("The maximum number of results to return. Defaults to 50."),
|
|
803
|
+
top: z.coerce.number().default(50).describe("The maximum number of results to return. Defaults to 50."),
|
|
680
804
|
responseType: z.enum(["full", "ids"]).default("full").describe("Response type: 'full' returns complete query results (default), 'ids' returns only work item IDs for reduced payload size."),
|
|
681
805
|
}, async ({ id, project, team, timePrecision, top, responseType }) => {
|
|
682
806
|
try {
|
|
@@ -708,10 +832,14 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
708
832
|
updates: z
|
|
709
833
|
.array(z.object({
|
|
710
834
|
op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform on the field."),
|
|
711
|
-
id: z.number().describe("The ID of the work item to update."),
|
|
835
|
+
id: z.coerce.number().min(1).describe("The ID of the work item to update."),
|
|
712
836
|
path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
|
|
713
837
|
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."),
|
|
714
|
-
format: z
|
|
838
|
+
format: z
|
|
839
|
+
.enum(["Html", "Markdown"])
|
|
840
|
+
.optional()
|
|
841
|
+
.default("Markdown")
|
|
842
|
+
.describe("The format of the field value. Only to be used for large text fields. e.g., 'Html', 'Markdown'. Optional, defaults to 'Markdown'."),
|
|
715
843
|
}))
|
|
716
844
|
.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)."),
|
|
717
845
|
}, async ({ updates }) => {
|
|
@@ -772,12 +900,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
772
900
|
};
|
|
773
901
|
}
|
|
774
902
|
});
|
|
775
|
-
server.tool(WORKITEM_TOOLS.work_items_link, "Link work items together in batch.", {
|
|
776
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
903
|
+
server.tool(WORKITEM_TOOLS.work_items_link, "Link work items together in batch. If a project is not specified, you will be prompted to select one.", {
|
|
904
|
+
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."),
|
|
777
905
|
updates: z
|
|
778
906
|
.array(z.object({
|
|
779
|
-
id: z.number().describe("The ID of the work item to update."),
|
|
780
|
-
linkToId: z.number().describe("The ID of the work item to link to."),
|
|
907
|
+
id: z.coerce.number().min(1).describe("The ID of the work item to update."),
|
|
908
|
+
linkToId: z.coerce.number().min(1).describe("The ID of the work item to link to."),
|
|
781
909
|
type: z
|
|
782
910
|
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by"])
|
|
783
911
|
.default("related")
|
|
@@ -788,6 +916,13 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
788
916
|
}, async ({ project, updates }) => {
|
|
789
917
|
try {
|
|
790
918
|
const connection = await connectionProvider();
|
|
919
|
+
let resolvedProject = project;
|
|
920
|
+
if (!resolvedProject) {
|
|
921
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to link work items in.");
|
|
922
|
+
if ("response" in result)
|
|
923
|
+
return result.response;
|
|
924
|
+
resolvedProject = result.resolved;
|
|
925
|
+
}
|
|
791
926
|
const orgUrl = connection.serverUrl;
|
|
792
927
|
const accessToken = await tokenProvider();
|
|
793
928
|
// Extract unique IDs from the updates array
|
|
@@ -805,7 +940,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
805
940
|
path: "/relations/-",
|
|
806
941
|
value: {
|
|
807
942
|
rel: `${getLinkTypeFromName(type)}`,
|
|
808
|
-
url: `${orgUrl}/${
|
|
943
|
+
url: `${orgUrl}/${resolvedProject}/_apis/wit/workItems/${linkToId}`,
|
|
809
944
|
attributes: {
|
|
810
945
|
comment: comment || "",
|
|
811
946
|
},
|
|
@@ -837,9 +972,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
837
972
|
};
|
|
838
973
|
}
|
|
839
974
|
});
|
|
840
|
-
server.tool(WORKITEM_TOOLS.work_item_unlink, "Remove one or many links from a single work item", {
|
|
841
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
842
|
-
id: z.number().describe("The ID of the work item to remove the links from."),
|
|
975
|
+
server.tool(WORKITEM_TOOLS.work_item_unlink, "Remove one or many links from a single work item. If a project is not specified, you will be prompted to select one.", {
|
|
976
|
+
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."),
|
|
977
|
+
id: z.coerce.number().min(1).describe("The ID of the work item to remove the links from."),
|
|
843
978
|
type: z
|
|
844
979
|
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "artifact"])
|
|
845
980
|
.default("related")
|
|
@@ -848,14 +983,21 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
848
983
|
}, async ({ project, id, type, url }) => {
|
|
849
984
|
try {
|
|
850
985
|
const connection = await connectionProvider();
|
|
986
|
+
let resolvedProject = project;
|
|
987
|
+
if (!resolvedProject) {
|
|
988
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to unlink work items in.");
|
|
989
|
+
if ("response" in result)
|
|
990
|
+
return result.response;
|
|
991
|
+
resolvedProject = result.resolved;
|
|
992
|
+
}
|
|
851
993
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
852
|
-
const workItem = await workItemApi.getWorkItem(id, undefined, undefined, WorkItemExpand.Relations,
|
|
994
|
+
const workItem = await workItemApi.getWorkItem(id, undefined, undefined, WorkItemExpand.Relations, resolvedProject);
|
|
853
995
|
const relations = workItem.relations ?? [];
|
|
854
996
|
const linkType = getLinkTypeFromName(type);
|
|
855
997
|
let relationIndexes = [];
|
|
856
998
|
if (url && url.trim().length > 0) {
|
|
857
999
|
// If url is provided, find relations matching both rel type and url
|
|
858
|
-
relationIndexes = relations.map((relation, idx) => (relation.url === url ? idx : -1)).filter((idx) => idx !== -1);
|
|
1000
|
+
relationIndexes = relations.map((relation, idx) => (relation.rel === linkType && relation.url === url ? idx : -1)).filter((idx) => idx !== -1);
|
|
859
1001
|
}
|
|
860
1002
|
else {
|
|
861
1003
|
// If url is not provided, find all relations matching rel type
|
|
@@ -875,7 +1017,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
875
1017
|
op: "remove",
|
|
876
1018
|
path: `/relations/${idx}`,
|
|
877
1019
|
}));
|
|
878
|
-
const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id,
|
|
1020
|
+
const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id, resolvedProject);
|
|
879
1021
|
return {
|
|
880
1022
|
content: [
|
|
881
1023
|
{
|
|
@@ -901,9 +1043,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
901
1043
|
};
|
|
902
1044
|
}
|
|
903
1045
|
});
|
|
904
|
-
server.tool(WORKITEM_TOOLS.add_artifact_link, "Add artifact links (repository, branch, commit, builds) to work items. You can either provide the full vstfs URI or the individual components to build it automatically.", {
|
|
905
|
-
workItemId: z.number().describe("The ID of the work item to add the artifact link to."),
|
|
906
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
1046
|
+
server.tool(WORKITEM_TOOLS.add_artifact_link, "Add artifact links (repository, branch, commit, builds) to work items. You can either provide the full vstfs URI or the individual components to build it automatically. If a project is not specified, you will be prompted to select one.", {
|
|
1047
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item to add the artifact link to."),
|
|
1048
|
+
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."),
|
|
907
1049
|
// Option 1: Provide full URI directly
|
|
908
1050
|
artifactUri: z.string().optional().describe("The complete VSTFS URI of the artifact to link. If provided, individual component parameters are ignored."),
|
|
909
1051
|
// Option 2: Provide individual components to build URI automatically based on linkType
|
|
@@ -911,8 +1053,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
911
1053
|
repositoryId: z.string().optional().describe("The repository ID (GUID) containing the artifact. Required for Git artifacts when artifactUri is not provided."),
|
|
912
1054
|
branchName: z.string().optional().describe("The branch name (e.g., 'main'). Required when linkType is 'Branch'."),
|
|
913
1055
|
commitId: z.string().optional().describe("The commit SHA hash. Required when linkType is 'Fixed in Commit'."),
|
|
914
|
-
pullRequestId: z.number().optional().describe("The pull request ID. Required when linkType is 'Pull Request'."),
|
|
915
|
-
buildId: z.number().optional().describe("The build ID. Required when linkType is 'Build', 'Found in build', or 'Integrated in build'."),
|
|
1056
|
+
pullRequestId: z.coerce.number().min(1).optional().describe("The pull request ID. Required when linkType is 'Pull Request'."),
|
|
1057
|
+
buildId: z.coerce.number().min(1).optional().describe("The build ID. Required when linkType is 'Build', 'Found in build', or 'Integrated in build'."),
|
|
916
1058
|
linkType: z
|
|
917
1059
|
.enum([
|
|
918
1060
|
"Branch",
|
|
@@ -936,6 +1078,13 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
936
1078
|
}, async ({ workItemId, project, artifactUri, projectId, repositoryId, branchName, commitId, pullRequestId, buildId, linkType, comment }) => {
|
|
937
1079
|
try {
|
|
938
1080
|
const connection = await connectionProvider();
|
|
1081
|
+
let resolvedProject = project;
|
|
1082
|
+
if (!resolvedProject) {
|
|
1083
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to add the artifact link in.");
|
|
1084
|
+
if ("response" in result)
|
|
1085
|
+
return result.response;
|
|
1086
|
+
resolvedProject = result.resolved;
|
|
1087
|
+
}
|
|
939
1088
|
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
940
1089
|
let finalArtifactUri;
|
|
941
1090
|
if (artifactUri) {
|
|
@@ -1006,7 +1155,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
1006
1155
|
},
|
|
1007
1156
|
];
|
|
1008
1157
|
// Use the WorkItem API to update the work item with the new relation
|
|
1009
|
-
const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId,
|
|
1158
|
+
const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, resolvedProject);
|
|
1010
1159
|
if (!workItem) {
|
|
1011
1160
|
return { content: [{ type: "text", text: "Work item update failed" }], isError: true };
|
|
1012
1161
|
}
|
|
@@ -1033,5 +1182,96 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
1033
1182
|
};
|
|
1034
1183
|
}
|
|
1035
1184
|
});
|
|
1185
|
+
server.tool(WORKITEM_TOOLS.query_by_wiql, "Execute a WIQL (Work Item Query Language) query and return the matching work items. If a project is not specified, you will be prompted to select one.", {
|
|
1186
|
+
wiql: z.string().max(32768).describe('The WIQL query string to execute, e.g., "SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.TeamProject] = @project"'),
|
|
1187
|
+
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."),
|
|
1188
|
+
team: z.string().optional().describe("The name or ID of the Azure DevOps team. If not provided, the default team context will be used."),
|
|
1189
|
+
timePrecision: z.boolean().optional().describe("Whether to include time precision in date fields. Defaults to false."),
|
|
1190
|
+
top: z.coerce.number().default(50).describe("The maximum number of results to return. Defaults to 50."),
|
|
1191
|
+
}, async ({ wiql, project, team, timePrecision, top }) => {
|
|
1192
|
+
try {
|
|
1193
|
+
const connection = await connectionProvider();
|
|
1194
|
+
let resolvedProject = project;
|
|
1195
|
+
if (!resolvedProject) {
|
|
1196
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to run the WIQL query against.");
|
|
1197
|
+
if ("response" in result)
|
|
1198
|
+
return result.response;
|
|
1199
|
+
resolvedProject = result.resolved;
|
|
1200
|
+
}
|
|
1201
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
1202
|
+
const teamContext = { project: resolvedProject, team };
|
|
1203
|
+
const queryResult = await workItemApi.queryByWiql({ query: wiql }, teamContext, timePrecision, top);
|
|
1204
|
+
return createExternalContentResponse(queryResult, "wiql query results");
|
|
1205
|
+
}
|
|
1206
|
+
catch (error) {
|
|
1207
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
1208
|
+
return {
|
|
1209
|
+
content: [{ type: "text", text: `Error executing WIQL query: ${errorMessage}` }],
|
|
1210
|
+
isError: true,
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
server.tool(WORKITEM_TOOLS.get_work_item_attachment, "Download a work item attachment by its ID and return the content as a base64-encoded resource. Useful for viewing images (e.g. screenshots) attached to work items such as bugs. If a project is not specified, you will be prompted to select one.", {
|
|
1215
|
+
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."),
|
|
1216
|
+
attachmentId: z.string().describe("The GUID of the attachment. Found in the attachment URL: https://dev.azure.com/{org}/{project}/_apis/wit/attachments/{attachmentId}"),
|
|
1217
|
+
fileName: z.string().optional().describe("The file name of the attachment, e.g. 'screenshot.png'. Used to determine the MIME type for the returned resource."),
|
|
1218
|
+
}, async ({ project, attachmentId, fileName }) => {
|
|
1219
|
+
try {
|
|
1220
|
+
const connection = await connectionProvider();
|
|
1221
|
+
let resolvedProject = project;
|
|
1222
|
+
if (!resolvedProject) {
|
|
1223
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the work item attachment from.");
|
|
1224
|
+
if ("response" in result)
|
|
1225
|
+
return result.response;
|
|
1226
|
+
resolvedProject = result.resolved;
|
|
1227
|
+
}
|
|
1228
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
1229
|
+
const stream = await workItemApi.getAttachmentContent(attachmentId, fileName, resolvedProject);
|
|
1230
|
+
const chunks = [];
|
|
1231
|
+
await new Promise((resolve, reject) => {
|
|
1232
|
+
stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
1233
|
+
stream.on("end", resolve);
|
|
1234
|
+
stream.on("error", reject);
|
|
1235
|
+
});
|
|
1236
|
+
const buffer = Buffer.concat(chunks);
|
|
1237
|
+
const base64Data = buffer.toString("base64");
|
|
1238
|
+
const mimeType = getMimeType(fileName);
|
|
1239
|
+
return {
|
|
1240
|
+
content: [
|
|
1241
|
+
{
|
|
1242
|
+
type: "resource",
|
|
1243
|
+
resource: {
|
|
1244
|
+
uri: `data:${mimeType};base64,${base64Data}`,
|
|
1245
|
+
mimeType,
|
|
1246
|
+
blob: base64Data,
|
|
1247
|
+
},
|
|
1248
|
+
},
|
|
1249
|
+
],
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
catch (error) {
|
|
1253
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
1254
|
+
return {
|
|
1255
|
+
content: [{ type: "text", text: `Error retrieving work item attachment: ${errorMessage}` }],
|
|
1256
|
+
isError: true,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
function getMimeType(fileName) {
|
|
1262
|
+
const ext = fileName?.split(".").pop()?.toLowerCase();
|
|
1263
|
+
const mimeTypes = {
|
|
1264
|
+
png: "image/png",
|
|
1265
|
+
jpg: "image/jpeg",
|
|
1266
|
+
jpeg: "image/jpeg",
|
|
1267
|
+
gif: "image/gif",
|
|
1268
|
+
bmp: "image/bmp",
|
|
1269
|
+
svg: "image/svg+xml",
|
|
1270
|
+
webp: "image/webp",
|
|
1271
|
+
pdf: "application/pdf",
|
|
1272
|
+
txt: "text/plain",
|
|
1273
|
+
zip: "application/zip",
|
|
1274
|
+
};
|
|
1275
|
+
return (ext && mimeTypes[ext]) ?? "application/octet-stream";
|
|
1036
1276
|
}
|
|
1037
1277
|
export { WORKITEM_TOOLS, configureWorkItemTools };
|