@azure-devops/mcp 0.1.0 → 0.2.0-preview-oauth
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 -21
- package/README.md +320 -247
- package/dist/auth.js +74 -0
- package/dist/domains.js +1 -0
- package/dist/http.js +52 -0
- package/dist/index.js +61 -28
- package/dist/org-tenants.js +73 -0
- package/dist/orgtenants.js +73 -0
- package/dist/prompts.js +35 -10
- package/dist/server.js +36 -0
- package/dist/shared/domains.js +122 -0
- package/dist/shared/tool-validation.js +92 -0
- package/dist/tenant.js +73 -0
- package/dist/tools/advanced-security.js +108 -0
- package/dist/tools/advsec.js +108 -0
- package/dist/tools/auth.js +46 -4
- package/dist/tools/builds.js +146 -21
- package/dist/tools/core.js +73 -14
- package/dist/tools/releases.js +40 -15
- package/dist/tools/repos.js +421 -54
- package/dist/tools/repositories.js +666 -0
- package/dist/tools/search.js +100 -89
- package/dist/tools/test-plans.js +213 -0
- package/dist/tools/testplans.js +22 -21
- package/dist/tools/wiki.js +295 -37
- package/dist/tools/work-items.js +809 -0
- package/dist/tools/work.js +83 -39
- package/dist/tools/workitems.js +495 -171
- package/dist/tools.js +24 -14
- package/dist/useragent.js +20 -0
- package/dist/utils.js +52 -2
- package/dist/version.js +1 -1
- package/package.json +65 -55
package/dist/tools/workitems.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
+
import { WorkItemExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
4
|
+
import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
3
5
|
import { z } from "zod";
|
|
4
|
-
import { batchApiVersion,
|
|
6
|
+
import { batchApiVersion, markdownCommentsApiVersion, getEnumKeys, safeEnumConvert } from "../utils.js";
|
|
5
7
|
const WORKITEM_TOOLS = {
|
|
6
8
|
my_work_items: "wit_my_work_items",
|
|
7
9
|
list_backlogs: "wit_list_backlogs",
|
|
@@ -13,14 +15,15 @@ const WORKITEM_TOOLS = {
|
|
|
13
15
|
list_work_item_comments: "wit_list_work_item_comments",
|
|
14
16
|
get_work_items_for_iteration: "wit_get_work_items_for_iteration",
|
|
15
17
|
add_work_item_comment: "wit_add_work_item_comment",
|
|
16
|
-
|
|
18
|
+
add_child_work_items: "wit_add_child_work_items",
|
|
17
19
|
link_work_item_to_pull_request: "wit_link_work_item_to_pull_request",
|
|
18
20
|
get_work_item_type: "wit_get_work_item_type",
|
|
19
21
|
get_query: "wit_get_query",
|
|
20
22
|
get_query_results_by_id: "wit_get_query_results_by_id",
|
|
21
23
|
update_work_items_batch: "wit_update_work_items_batch",
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
work_items_link: "wit_work_items_link",
|
|
25
|
+
work_item_unlink: "wit_work_item_unlink",
|
|
26
|
+
add_artifact_link: "wit_add_artifact_link",
|
|
24
27
|
};
|
|
25
28
|
function getLinkTypeFromName(name) {
|
|
26
29
|
switch (name.toLowerCase()) {
|
|
@@ -42,14 +45,20 @@ function getLinkTypeFromName(name) {
|
|
|
42
45
|
return "Microsoft.VSTS.Common.TestedBy-Forward";
|
|
43
46
|
case "tests":
|
|
44
47
|
return "Microsoft.VSTS.Common.TestedBy-Reverse";
|
|
48
|
+
case "affects":
|
|
49
|
+
return "Microsoft.VSTS.Common.Affects-Forward";
|
|
50
|
+
case "affected by":
|
|
51
|
+
return "Microsoft.VSTS.Common.Affects-Reverse";
|
|
52
|
+
case "artifact":
|
|
53
|
+
return "ArtifactLink";
|
|
45
54
|
default:
|
|
46
55
|
throw new Error(`Unknown link type: ${name}`);
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
|
-
function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
58
|
+
function configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
50
59
|
server.tool(WORKITEM_TOOLS.list_backlogs, "Revieve a list of backlogs for a given project and team.", {
|
|
51
60
|
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.")
|
|
61
|
+
team: z.string().describe("The name or ID of the Azure DevOps team."),
|
|
53
62
|
}, async ({ project, team }) => {
|
|
54
63
|
const connection = await connectionProvider();
|
|
55
64
|
const workApi = await connection.getWorkApi();
|
|
@@ -62,7 +71,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
62
71
|
server.tool(WORKITEM_TOOLS.list_backlog_work_items, "Retrieve a list of backlogs of for a given project, team, and backlog category", {
|
|
63
72
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
64
73
|
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.")
|
|
74
|
+
backlogId: z.string().describe("The ID of the backlog category to retrieve work items from."),
|
|
66
75
|
}, async ({ project, team, backlogId }) => {
|
|
67
76
|
const connection = await connectionProvider();
|
|
68
77
|
const workApi = await connection.getWorkApi();
|
|
@@ -87,12 +96,41 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
87
96
|
});
|
|
88
97
|
server.tool(WORKITEM_TOOLS.get_work_items_batch_by_ids, "Retrieve list of work items by IDs in batch.", {
|
|
89
98
|
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
|
-
|
|
99
|
+
ids: z.array(z.number()).describe("The IDs of the work items to retrieve."),
|
|
100
|
+
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."),
|
|
101
|
+
}, async ({ project, ids, fields }) => {
|
|
92
102
|
const connection = await connectionProvider();
|
|
93
103
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
94
|
-
const
|
|
95
|
-
|
|
104
|
+
const defaultFields = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank", "System.AssignedTo"];
|
|
105
|
+
// If no fields are provided, use the default set of fields
|
|
106
|
+
const fieldsToUse = !fields || fields.length === 0 ? defaultFields : fields;
|
|
107
|
+
const workitems = await workItemApi.getWorkItemsBatch({ ids, fields: fieldsToUse }, project);
|
|
108
|
+
// List of identity fields that need to be transformed from objects to formatted strings
|
|
109
|
+
const identityFields = [
|
|
110
|
+
"System.AssignedTo",
|
|
111
|
+
"System.CreatedBy",
|
|
112
|
+
"System.ChangedBy",
|
|
113
|
+
"System.AuthorizedAs",
|
|
114
|
+
"Microsoft.VSTS.Common.ActivatedBy",
|
|
115
|
+
"Microsoft.VSTS.Common.ResolvedBy",
|
|
116
|
+
"Microsoft.VSTS.Common.ClosedBy",
|
|
117
|
+
];
|
|
118
|
+
// Format identity fields to include displayName and uniqueName
|
|
119
|
+
// Removing the identity object as the response. It's too much and not needed
|
|
120
|
+
if (workitems && Array.isArray(workitems)) {
|
|
121
|
+
workitems.forEach((item) => {
|
|
122
|
+
if (item.fields) {
|
|
123
|
+
identityFields.forEach((fieldName) => {
|
|
124
|
+
if (item.fields && item.fields[fieldName] && typeof item.fields[fieldName] === "object") {
|
|
125
|
+
const identityField = item.fields[fieldName];
|
|
126
|
+
const name = identityField.displayName || "";
|
|
127
|
+
const email = identityField.uniqueName || "";
|
|
128
|
+
item.fields[fieldName] = `${name} <${email}>`.trim();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
96
134
|
return {
|
|
97
135
|
content: [{ type: "text", text: JSON.stringify(workitems, null, 2) }],
|
|
98
136
|
};
|
|
@@ -101,11 +139,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
101
139
|
id: z.number().describe("The ID of the work item to retrieve."),
|
|
102
140
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
103
141
|
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."),
|
|
142
|
+
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."),
|
|
105
143
|
expand: z
|
|
106
144
|
.enum(["all", "fields", "links", "none", "relations"])
|
|
107
145
|
.describe("Optional expand parameter to include additional details in the response.")
|
|
108
|
-
.optional()
|
|
146
|
+
.optional()
|
|
147
|
+
.describe("Expand options include 'all', 'fields', 'links', 'none', and 'relations'. Relations can be used to get child workitems. Defaults to 'none'."),
|
|
109
148
|
}, async ({ id, project, fields, asOf, expand }) => {
|
|
110
149
|
const connection = await connectionProvider();
|
|
111
150
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
@@ -117,7 +156,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
117
156
|
server.tool(WORKITEM_TOOLS.list_work_item_comments, "Retrieve list of comments for a work item by ID.", {
|
|
118
157
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
119
158
|
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.")
|
|
159
|
+
top: z.number().default(50).describe("Optional number of comments to retrieve. Defaults to all comments."),
|
|
121
160
|
}, async ({ project, workItemId, top }) => {
|
|
122
161
|
const connection = await connectionProvider();
|
|
123
162
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
@@ -129,82 +168,160 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
129
168
|
server.tool(WORKITEM_TOOLS.add_work_item_comment, "Add comment to a work item by ID.", {
|
|
130
169
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
131
170
|
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
|
-
|
|
171
|
+
comment: z.string().describe("The text of the comment to add to the work item."),
|
|
172
|
+
format: z.enum(["markdown", "html"]).optional().default("html"),
|
|
173
|
+
}, async ({ project, workItemId, comment, format }) => {
|
|
134
174
|
const connection = await connectionProvider();
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const
|
|
175
|
+
const orgUrl = connection.serverUrl;
|
|
176
|
+
const accessToken = await tokenProvider();
|
|
177
|
+
const body = {
|
|
178
|
+
text: comment,
|
|
179
|
+
};
|
|
180
|
+
const formatParameter = format === "markdown" ? 0 : 1;
|
|
181
|
+
const response = await fetch(`${orgUrl}/${project}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: {
|
|
184
|
+
"Authorization": `Bearer ${accessToken.token}`,
|
|
185
|
+
"Content-Type": "application/json",
|
|
186
|
+
"User-Agent": userAgentProvider(),
|
|
187
|
+
},
|
|
188
|
+
body: JSON.stringify(body),
|
|
189
|
+
});
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
throw new Error(`Failed to add a work item comment: ${response.statusText}}`);
|
|
192
|
+
}
|
|
193
|
+
const comments = await response.text();
|
|
138
194
|
return {
|
|
139
|
-
content: [
|
|
140
|
-
{ type: "text", text: JSON.stringify(commentResponse, null, 2) },
|
|
141
|
-
],
|
|
195
|
+
content: [{ type: "text", text: comments }],
|
|
142
196
|
};
|
|
143
197
|
});
|
|
144
|
-
server.tool(WORKITEM_TOOLS.
|
|
198
|
+
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.", {
|
|
145
199
|
parentId: z.number().describe("The ID of the parent work item to create a child work item under."),
|
|
146
200
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
147
201
|
workItemType: z.string().describe("The type of the child work item to create."),
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
{
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
202
|
+
items: z.array(z.object({
|
|
203
|
+
title: z.string().describe("The title of the child work item."),
|
|
204
|
+
description: z.string().describe("The description of the child work item."),
|
|
205
|
+
format: z.enum(["Markdown", "Html"]).default("Html").describe("Format for the description on the child work item, e.g., 'Markdown', 'Html'. Defaults to 'Html'."),
|
|
206
|
+
areaPath: z.string().optional().describe("Optional area path for the child work item."),
|
|
207
|
+
iterationPath: z.string().optional().describe("Optional iteration path for the child work item."),
|
|
208
|
+
})),
|
|
209
|
+
}, async ({ parentId, project, workItemType, items }) => {
|
|
210
|
+
try {
|
|
211
|
+
const connection = await connectionProvider();
|
|
212
|
+
const orgUrl = connection.serverUrl;
|
|
213
|
+
const accessToken = await tokenProvider();
|
|
214
|
+
if (items.length > 50) {
|
|
215
|
+
return {
|
|
216
|
+
content: [{ type: "text", text: `A maximum of 50 child work items can be created in a single call.` }],
|
|
217
|
+
isError: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const body = items.map((item, x) => {
|
|
221
|
+
const ops = [
|
|
222
|
+
{
|
|
223
|
+
op: "add",
|
|
224
|
+
path: "/id",
|
|
225
|
+
value: `-${x + 1}`,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
op: "add",
|
|
229
|
+
path: "/fields/System.Title",
|
|
230
|
+
value: item.title,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
op: "add",
|
|
234
|
+
path: "/fields/System.Description",
|
|
235
|
+
value: item.description,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
op: "add",
|
|
239
|
+
path: "/fields/Microsoft.VSTS.TCM.ReproSteps",
|
|
240
|
+
value: item.description,
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
op: "add",
|
|
244
|
+
path: "/relations/-",
|
|
245
|
+
value: {
|
|
246
|
+
rel: "System.LinkTypes.Hierarchy-Reverse",
|
|
247
|
+
url: `${connection.serverUrl}/${project}/_apis/wit/workItems/${parentId}`,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
if (item.areaPath && item.areaPath.trim().length > 0) {
|
|
252
|
+
ops.push({
|
|
253
|
+
op: "add",
|
|
254
|
+
path: "/fields/System.AreaPath",
|
|
255
|
+
value: item.areaPath,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (item.iterationPath && item.iterationPath.trim().length > 0) {
|
|
259
|
+
ops.push({
|
|
260
|
+
op: "add",
|
|
261
|
+
path: "/fields/System.IterationPath",
|
|
262
|
+
value: item.iterationPath,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (item.format && item.format === "Markdown") {
|
|
266
|
+
ops.push({
|
|
267
|
+
op: "add",
|
|
268
|
+
path: "/multilineFieldsFormat/System.Description",
|
|
269
|
+
value: item.format,
|
|
270
|
+
});
|
|
271
|
+
ops.push({
|
|
272
|
+
op: "add",
|
|
273
|
+
path: "/multilineFieldsFormat/Microsoft.VSTS.TCM.ReproSteps",
|
|
274
|
+
value: item.format,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
method: "PATCH",
|
|
279
|
+
uri: `/${project}/_apis/wit/workitems/$${workItemType}?api-version=${batchApiVersion}`,
|
|
280
|
+
headers: {
|
|
281
|
+
"Content-Type": "application/json-patch+json",
|
|
282
|
+
},
|
|
283
|
+
body: ops,
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
|
|
287
|
+
method: "PATCH",
|
|
288
|
+
headers: {
|
|
289
|
+
"Authorization": `Bearer ${accessToken.token}`,
|
|
290
|
+
"Content-Type": "application/json",
|
|
291
|
+
"User-Agent": userAgentProvider(),
|
|
172
292
|
},
|
|
173
|
-
|
|
174
|
-
];
|
|
175
|
-
if (areaPath && areaPath.trim().length > 0) {
|
|
176
|
-
document.push({
|
|
177
|
-
op: "add",
|
|
178
|
-
path: "/fields/System.AreaPath",
|
|
179
|
-
value: areaPath,
|
|
293
|
+
body: JSON.stringify(body),
|
|
180
294
|
});
|
|
295
|
+
if (!response.ok) {
|
|
296
|
+
throw new Error(`Failed to update work items in batch: ${response.statusText}`);
|
|
297
|
+
}
|
|
298
|
+
const result = await response.json();
|
|
299
|
+
return {
|
|
300
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
301
|
+
};
|
|
181
302
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: `Error creating child work items: ${errorMessage}` }],
|
|
307
|
+
isError: true,
|
|
308
|
+
};
|
|
188
309
|
}
|
|
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
310
|
});
|
|
196
311
|
server.tool(WORKITEM_TOOLS.link_work_item_to_pull_request, "Link a single work item to an existing pull request.", {
|
|
197
|
-
|
|
312
|
+
projectId: z.string().describe("The project ID of the Azure DevOps project (note: project name is not valid)."),
|
|
198
313
|
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
314
|
pullRequestId: z.number().describe("The ID of the pull request to link to."),
|
|
200
315
|
workItemId: z.number().describe("The ID of the work item to link to the pull request."),
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
316
|
+
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)."),
|
|
317
|
+
}, async ({ projectId, repositoryId, pullRequestId, workItemId, pullRequestProjectId }) => {
|
|
204
318
|
try {
|
|
319
|
+
const connection = await connectionProvider();
|
|
320
|
+
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
205
321
|
// Create artifact link relation using vstfs format
|
|
206
322
|
// Format: vstfs:///Git/PullRequestId/{project}/{repositoryId}/{pullRequestId}
|
|
207
|
-
const
|
|
323
|
+
const artifactProjectId = pullRequestProjectId && pullRequestProjectId.trim() !== "" ? pullRequestProjectId : projectId;
|
|
324
|
+
const artifactPathValue = `${artifactProjectId}/${repositoryId}/${pullRequestId}`;
|
|
208
325
|
const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`;
|
|
209
326
|
// Use the PATCH document format for adding a relation
|
|
210
327
|
const patchDocument = [
|
|
@@ -221,7 +338,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
221
338
|
},
|
|
222
339
|
];
|
|
223
340
|
// Use the WorkItem API to update the work item with the new relation
|
|
224
|
-
await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId,
|
|
341
|
+
const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, projectId);
|
|
342
|
+
if (!workItem) {
|
|
343
|
+
return { content: [{ type: "text", text: "Work item update failed" }], isError: true };
|
|
344
|
+
}
|
|
225
345
|
return {
|
|
226
346
|
content: [
|
|
227
347
|
{
|
|
@@ -236,18 +356,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
236
356
|
};
|
|
237
357
|
}
|
|
238
358
|
catch (error) {
|
|
239
|
-
|
|
359
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
240
360
|
return {
|
|
241
|
-
content: [
|
|
242
|
-
|
|
243
|
-
type: "text",
|
|
244
|
-
text: JSON.stringify({
|
|
245
|
-
workItemId,
|
|
246
|
-
pullRequestId,
|
|
247
|
-
success: false,
|
|
248
|
-
}, null, 2),
|
|
249
|
-
},
|
|
250
|
-
],
|
|
361
|
+
content: [{ type: "text", text: `Error linking work item to pull request: ${errorMessage}` }],
|
|
362
|
+
isError: true,
|
|
251
363
|
};
|
|
252
364
|
}
|
|
253
365
|
});
|
|
@@ -266,19 +378,29 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
266
378
|
});
|
|
267
379
|
server.tool(WORKITEM_TOOLS.update_work_item, "Update a work item by ID with specified fields.", {
|
|
268
380
|
id: z.number().describe("The ID of the work item to update."),
|
|
269
|
-
updates: z
|
|
270
|
-
|
|
381
|
+
updates: z
|
|
382
|
+
.array(z.object({
|
|
383
|
+
op: z
|
|
384
|
+
.string()
|
|
385
|
+
.transform((val) => val.toLowerCase())
|
|
386
|
+
.pipe(z.enum(["add", "replace", "remove"]))
|
|
387
|
+
.default("add")
|
|
388
|
+
.describe("The operation to perform on the field."),
|
|
271
389
|
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 '
|
|
273
|
-
}))
|
|
390
|
+
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."),
|
|
391
|
+
}))
|
|
392
|
+
.describe("An array of field updates to apply to the work item."),
|
|
274
393
|
}, async ({ id, updates }) => {
|
|
275
394
|
const connection = await connectionProvider();
|
|
276
395
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
277
|
-
|
|
396
|
+
// Convert operation names to lowercase for API
|
|
397
|
+
const apiUpdates = updates.map((update) => ({
|
|
398
|
+
...update,
|
|
399
|
+
op: update.op,
|
|
400
|
+
}));
|
|
401
|
+
const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id);
|
|
278
402
|
return {
|
|
279
|
-
content: [
|
|
280
|
-
{ type: "text", text: JSON.stringify(updatedWorkItem, null, 2) },
|
|
281
|
-
],
|
|
403
|
+
content: [{ type: "text", text: JSON.stringify(updatedWorkItem, null, 2) }],
|
|
282
404
|
};
|
|
283
405
|
});
|
|
284
406
|
server.tool(WORKITEM_TOOLS.get_work_item_type, "Get a specific work item type.", {
|
|
@@ -289,43 +411,72 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
289
411
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
290
412
|
const workItemTypeInfo = await workItemApi.getWorkItemType(project, workItemType);
|
|
291
413
|
return {
|
|
292
|
-
content: [
|
|
293
|
-
{ type: "text", text: JSON.stringify(workItemTypeInfo, null, 2) },
|
|
294
|
-
],
|
|
414
|
+
content: [{ type: "text", text: JSON.stringify(workItemTypeInfo, null, 2) }],
|
|
295
415
|
};
|
|
296
416
|
});
|
|
297
417
|
server.tool(WORKITEM_TOOLS.create_work_item, "Create a new work item in a specified project and work item type.", {
|
|
298
418
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
299
419
|
workItemType: z.string().describe("The type of work item to create, e.g., 'Task', 'Bug', etc."),
|
|
300
|
-
fields: z
|
|
420
|
+
fields: z
|
|
421
|
+
.array(z.object({
|
|
422
|
+
name: z.string().describe("The name of the field, e.g., 'System.Title'."),
|
|
423
|
+
value: z.string().describe("The value of the field."),
|
|
424
|
+
format: z.enum(["Html", "Markdown"]).optional().describe("the format of the field value, e.g., 'Html', 'Markdown'. Optional, defaults to 'Html'."),
|
|
425
|
+
}))
|
|
426
|
+
.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."),
|
|
301
427
|
}, async ({ project, workItemType, fields }) => {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
428
|
+
try {
|
|
429
|
+
const connection = await connectionProvider();
|
|
430
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
431
|
+
const document = fields.map(({ name, value }) => ({
|
|
432
|
+
op: "add",
|
|
433
|
+
path: `/fields/${name}`,
|
|
434
|
+
value: value,
|
|
435
|
+
}));
|
|
436
|
+
// Check if any field has format === "Markdown" and add the multilineFieldsFormat operation
|
|
437
|
+
// this should only happen for large text fields, but since we dont't know by field name, lets assume if the users
|
|
438
|
+
// passes a value longer than 50 characters, then we can set the format to Markdown
|
|
439
|
+
fields.forEach(({ name, value, format }) => {
|
|
440
|
+
if (value.length > 50 && format === "Markdown") {
|
|
441
|
+
document.push({
|
|
442
|
+
op: "add",
|
|
443
|
+
path: `/multilineFieldsFormat/${name}`,
|
|
444
|
+
value: "Markdown",
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
const newWorkItem = await workItemApi.createWorkItem(null, document, project, workItemType);
|
|
449
|
+
if (!newWorkItem) {
|
|
450
|
+
return { content: [{ type: "text", text: "Work item was not created" }], isError: true };
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
content: [{ type: "text", text: JSON.stringify(newWorkItem, null, 2) }],
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
458
|
+
return {
|
|
459
|
+
content: [{ type: "text", text: `Error creating work item: ${errorMessage}` }],
|
|
460
|
+
isError: true,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
313
463
|
});
|
|
314
464
|
server.tool(WORKITEM_TOOLS.get_query, "Get a query by its ID or path.", {
|
|
315
465
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
316
466
|
query: z.string().describe("The ID or path of the query to retrieve."),
|
|
317
|
-
expand: z
|
|
467
|
+
expand: z
|
|
468
|
+
.enum(getEnumKeys(QueryExpand))
|
|
469
|
+
.optional()
|
|
470
|
+
.describe("Optional expand parameter to include additional details in the response. Defaults to 'None'."),
|
|
318
471
|
depth: z.number().default(0).describe("Optional depth parameter to specify how deep to expand the query. Defaults to 0."),
|
|
319
472
|
includeDeleted: z.boolean().default(false).describe("Whether to include deleted items in the query results. Defaults to false."),
|
|
320
473
|
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
|
|
474
|
+
}, async ({ project, query, expand, depth, includeDeleted, useIsoDateFormat }) => {
|
|
322
475
|
const connection = await connectionProvider();
|
|
323
476
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
324
|
-
const queryDetails = await workItemApi.getQuery(project, query, expand, depth, includeDeleted, useIsoDateFormat);
|
|
477
|
+
const queryDetails = await workItemApi.getQuery(project, query, safeEnumConvert(QueryExpand, expand), depth, includeDeleted, useIsoDateFormat);
|
|
325
478
|
return {
|
|
326
|
-
content: [
|
|
327
|
-
{ type: "text", text: JSON.stringify(queryDetails, null, 2) },
|
|
328
|
-
],
|
|
479
|
+
content: [{ type: "text", text: JSON.stringify(queryDetails, null, 2) }],
|
|
329
480
|
};
|
|
330
481
|
});
|
|
331
482
|
server.tool(WORKITEM_TOOLS.get_query_results_by_id, "Retrieve the results of a work item query given the query ID.", {
|
|
@@ -344,36 +495,53 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
344
495
|
};
|
|
345
496
|
});
|
|
346
497
|
server.tool(WORKITEM_TOOLS.update_work_items_batch, "Update work items in batch", {
|
|
347
|
-
updates: z
|
|
348
|
-
|
|
498
|
+
updates: z
|
|
499
|
+
.array(z.object({
|
|
500
|
+
op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform on the field."),
|
|
349
501
|
id: z.number().describe("The ID of the work item to update."),
|
|
350
502
|
path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
|
|
351
503
|
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
|
-
|
|
504
|
+
format: z.enum(["Html", "Markdown"]).optional().describe("The format of the field value. Only to be used for large text fields. e.g., 'Html', 'Markdown'. Optional, defaults to 'Html'."),
|
|
505
|
+
}))
|
|
506
|
+
.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
507
|
}, async ({ updates }) => {
|
|
354
508
|
const connection = await connectionProvider();
|
|
355
509
|
const orgUrl = connection.serverUrl;
|
|
356
510
|
const accessToken = await tokenProvider();
|
|
357
511
|
// Extract unique IDs from the updates array
|
|
358
512
|
const uniqueIds = Array.from(new Set(updates.map((update) => update.id)));
|
|
359
|
-
const body = uniqueIds.map((id) =>
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
headers: {
|
|
363
|
-
"Content-Type": "application/json-patch+json",
|
|
364
|
-
},
|
|
365
|
-
body: updates.filter((update) => update.id === id).map(({ op, path, value }) => ({
|
|
513
|
+
const body = uniqueIds.map((id) => {
|
|
514
|
+
const workItemUpdates = updates.filter((update) => update.id === id);
|
|
515
|
+
const operations = workItemUpdates.map(({ op, path, value }) => ({
|
|
366
516
|
op: op,
|
|
367
517
|
path: path,
|
|
368
518
|
value: value,
|
|
369
|
-
}))
|
|
370
|
-
|
|
519
|
+
}));
|
|
520
|
+
// Add format operations for Markdown fields
|
|
521
|
+
workItemUpdates.forEach(({ path, value, format }) => {
|
|
522
|
+
if (format === "Markdown" && value && value.length > 50) {
|
|
523
|
+
operations.push({
|
|
524
|
+
op: "Add",
|
|
525
|
+
path: `/multilineFieldsFormat${path.replace("/fields", "")}`,
|
|
526
|
+
value: "Markdown",
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
return {
|
|
531
|
+
method: "PATCH",
|
|
532
|
+
uri: `/_apis/wit/workitems/${id}?api-version=${batchApiVersion}`,
|
|
533
|
+
headers: {
|
|
534
|
+
"Content-Type": "application/json-patch+json",
|
|
535
|
+
},
|
|
536
|
+
body: operations,
|
|
537
|
+
};
|
|
538
|
+
});
|
|
371
539
|
const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
|
|
372
540
|
method: "PATCH",
|
|
373
541
|
headers: {
|
|
374
|
-
Authorization: `Bearer ${accessToken.token}`,
|
|
542
|
+
"Authorization": `Bearer ${accessToken.token}`,
|
|
375
543
|
"Content-Type": "application/json",
|
|
376
|
-
"User-Agent":
|
|
544
|
+
"User-Agent": userAgentProvider(),
|
|
377
545
|
},
|
|
378
546
|
body: JSON.stringify(body),
|
|
379
547
|
});
|
|
@@ -387,12 +555,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
387
555
|
});
|
|
388
556
|
server.tool(WORKITEM_TOOLS.work_items_link, "Link work items together in batch.", {
|
|
389
557
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
390
|
-
updates: z
|
|
558
|
+
updates: z
|
|
559
|
+
.array(z.object({
|
|
391
560
|
id: z.number().describe("The ID of the work item to update."),
|
|
392
561
|
linkToId: z.number().describe("The ID of the work item to link to."),
|
|
393
|
-
type: z
|
|
562
|
+
type: z
|
|
563
|
+
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by"])
|
|
564
|
+
.default("related")
|
|
565
|
+
.describe("Type of link to create between the work items. Options include 'parent', 'child', 'duplicate', 'duplicate of', 'related', 'successor', 'predecessor', 'tested by', 'tests', 'affects', and 'affected by'. Defaults to 'related'."),
|
|
394
566
|
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
|
-
}))
|
|
567
|
+
}))
|
|
568
|
+
.describe(""),
|
|
396
569
|
}, async ({ project, updates }) => {
|
|
397
570
|
const connection = await connectionProvider();
|
|
398
571
|
const orgUrl = connection.serverUrl;
|
|
@@ -405,7 +578,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
405
578
|
headers: {
|
|
406
579
|
"Content-Type": "application/json-patch+json",
|
|
407
580
|
},
|
|
408
|
-
body: updates
|
|
581
|
+
body: updates
|
|
582
|
+
.filter((update) => update.id === id)
|
|
583
|
+
.map(({ linkToId, type, comment }) => ({
|
|
409
584
|
op: "add",
|
|
410
585
|
path: "/relations/-",
|
|
411
586
|
value: {
|
|
@@ -414,15 +589,15 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
414
589
|
attributes: {
|
|
415
590
|
comment: comment || "",
|
|
416
591
|
},
|
|
417
|
-
}
|
|
592
|
+
},
|
|
418
593
|
})),
|
|
419
594
|
}));
|
|
420
595
|
const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
|
|
421
596
|
method: "PATCH",
|
|
422
597
|
headers: {
|
|
423
|
-
Authorization: `Bearer ${accessToken.token}`,
|
|
598
|
+
"Authorization": `Bearer ${accessToken.token}`,
|
|
424
599
|
"Content-Type": "application/json",
|
|
425
|
-
"User-Agent":
|
|
600
|
+
"User-Agent": userAgentProvider(),
|
|
426
601
|
},
|
|
427
602
|
body: JSON.stringify(body),
|
|
428
603
|
});
|
|
@@ -434,52 +609,201 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
|
|
|
434
609
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
435
610
|
};
|
|
436
611
|
});
|
|
437
|
-
server.tool(WORKITEM_TOOLS.
|
|
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."),
|
|
612
|
+
server.tool(WORKITEM_TOOLS.work_item_unlink, "Remove one or many links from a single work item", {
|
|
440
613
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
614
|
+
id: z.number().describe("The ID of the work item to remove the links from."),
|
|
615
|
+
type: z
|
|
616
|
+
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "artifact"])
|
|
617
|
+
.default("related")
|
|
618
|
+
.describe("Type of link to remove. Options include 'parent', 'child', 'duplicate', 'duplicate of', 'related', 'successor', 'predecessor', 'tested by', 'tests', 'affects', 'affected by', and 'artifact'. Defaults to 'related'."),
|
|
619
|
+
url: z.string().optional().describe("Optional URL to match for the link to remove. If not provided, all links of the specified type will be removed."),
|
|
620
|
+
}, async ({ project, id, type, url }) => {
|
|
621
|
+
try {
|
|
622
|
+
const connection = await connectionProvider();
|
|
623
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
624
|
+
const workItem = await workItemApi.getWorkItem(id, undefined, undefined, WorkItemExpand.Relations, project);
|
|
625
|
+
const relations = workItem.relations ?? [];
|
|
626
|
+
const linkType = getLinkTypeFromName(type);
|
|
627
|
+
let relationIndexes = [];
|
|
628
|
+
if (url && url.trim().length > 0) {
|
|
629
|
+
// If url is provided, find relations matching both rel type and url
|
|
630
|
+
relationIndexes = relations.map((relation, idx) => (relation.url === url ? idx : -1)).filter((idx) => idx !== -1);
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
// If url is not provided, find all relations matching rel type
|
|
634
|
+
relationIndexes = relations.map((relation, idx) => (relation.rel === linkType ? idx : -1)).filter((idx) => idx !== -1);
|
|
635
|
+
}
|
|
636
|
+
if (relationIndexes.length === 0) {
|
|
637
|
+
return {
|
|
638
|
+
content: [{ type: "text", text: `No matching relations found for link type '${type}'${url ? ` and URL '${url}'` : ""}.\n${JSON.stringify(relations, null, 2)}` }],
|
|
639
|
+
isError: true,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
// Get the relations that will be removed for logging
|
|
643
|
+
const removedRelations = relationIndexes.map((idx) => relations[idx]);
|
|
644
|
+
// Sort indexes in descending order to avoid index shifting when removing
|
|
645
|
+
relationIndexes.sort((a, b) => b - a);
|
|
646
|
+
const apiUpdates = relationIndexes.map((idx) => ({
|
|
647
|
+
op: "remove",
|
|
648
|
+
path: `/relations/${idx}`,
|
|
649
|
+
}));
|
|
650
|
+
const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id, project);
|
|
651
|
+
return {
|
|
652
|
+
content: [
|
|
653
|
+
{
|
|
654
|
+
type: "text",
|
|
655
|
+
text: `Removed ${removedRelations.length} link(s) of type '${type}':\n` +
|
|
656
|
+
JSON.stringify(removedRelations, null, 2) +
|
|
657
|
+
`\n\nUpdated work item result:\n` +
|
|
658
|
+
JSON.stringify(updatedWorkItem, null, 2),
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
isError: false,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
return {
|
|
666
|
+
content: [
|
|
667
|
+
{
|
|
668
|
+
type: "text",
|
|
669
|
+
text: `Error unlinking work item: ${error instanceof Error ? error.message : "Unknown error occurred"}`,
|
|
670
|
+
},
|
|
671
|
+
],
|
|
672
|
+
isError: true,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
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.", {
|
|
677
|
+
workItemId: z.number().describe("The ID of the work item to add the artifact link to."),
|
|
678
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
679
|
+
// Option 1: Provide full URI directly
|
|
680
|
+
artifactUri: z.string().optional().describe("The complete VSTFS URI of the artifact to link. If provided, individual component parameters are ignored."),
|
|
681
|
+
// Option 2: Provide individual components to build URI automatically based on linkType
|
|
682
|
+
projectId: z.string().optional().describe("The project ID (GUID) containing the artifact. Required for Git artifacts when artifactUri is not provided."),
|
|
683
|
+
repositoryId: z.string().optional().describe("The repository ID (GUID) containing the artifact. Required for Git artifacts when artifactUri is not provided."),
|
|
684
|
+
branchName: z.string().optional().describe("The branch name (e.g., 'main'). Required when linkType is 'Branch'."),
|
|
685
|
+
commitId: z.string().optional().describe("The commit SHA hash. Required when linkType is 'Fixed in Commit'."),
|
|
686
|
+
pullRequestId: z.number().optional().describe("The pull request ID. Required when linkType is 'Pull Request'."),
|
|
687
|
+
buildId: z.number().optional().describe("The build ID. Required when linkType is 'Build', 'Found in build', or 'Integrated in build'."),
|
|
688
|
+
linkType: z
|
|
689
|
+
.enum([
|
|
690
|
+
"Branch",
|
|
691
|
+
"Build",
|
|
692
|
+
"Fixed in Changeset",
|
|
693
|
+
"Fixed in Commit",
|
|
694
|
+
"Found in build",
|
|
695
|
+
"Integrated in build",
|
|
696
|
+
"Model Link",
|
|
697
|
+
"Pull Request",
|
|
698
|
+
"Related Workitem",
|
|
699
|
+
"Result Attachment",
|
|
700
|
+
"Source Code File",
|
|
701
|
+
"Tag",
|
|
702
|
+
"Test Result",
|
|
703
|
+
"Wiki",
|
|
704
|
+
])
|
|
705
|
+
.default("Branch")
|
|
706
|
+
.describe("Type of artifact link, defaults to 'Branch'. This determines both the link type and how to build the VSTFS URI from individual components."),
|
|
707
|
+
comment: z.string().optional().describe("Comment to include with the artifact link."),
|
|
708
|
+
}, async ({ workItemId, project, artifactUri, projectId, repositoryId, branchName, commitId, pullRequestId, buildId, linkType, comment }) => {
|
|
709
|
+
try {
|
|
710
|
+
const connection = await connectionProvider();
|
|
711
|
+
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
712
|
+
let finalArtifactUri;
|
|
713
|
+
if (artifactUri) {
|
|
714
|
+
// Use the provided full URI
|
|
715
|
+
finalArtifactUri = artifactUri;
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
// Build the URI from individual components based on linkType
|
|
719
|
+
switch (linkType) {
|
|
720
|
+
case "Branch":
|
|
721
|
+
if (!projectId || !repositoryId || !branchName) {
|
|
722
|
+
return {
|
|
723
|
+
content: [{ type: "text", text: "For 'Branch' links, 'projectId', 'repositoryId', and 'branchName' are required." }],
|
|
724
|
+
isError: true,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
finalArtifactUri = `vstfs:///Git/Ref/${encodeURIComponent(projectId)}%2F${encodeURIComponent(repositoryId)}%2FGB${encodeURIComponent(branchName)}`;
|
|
728
|
+
break;
|
|
729
|
+
case "Fixed in Commit":
|
|
730
|
+
if (!projectId || !repositoryId || !commitId) {
|
|
731
|
+
return {
|
|
732
|
+
content: [{ type: "text", text: "For 'Fixed in Commit' links, 'projectId', 'repositoryId', and 'commitId' are required." }],
|
|
733
|
+
isError: true,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
finalArtifactUri = `vstfs:///Git/Commit/${encodeURIComponent(projectId)}%2F${encodeURIComponent(repositoryId)}%2F${encodeURIComponent(commitId)}`;
|
|
737
|
+
break;
|
|
738
|
+
case "Pull Request":
|
|
739
|
+
if (!projectId || !repositoryId || pullRequestId === undefined) {
|
|
740
|
+
return {
|
|
741
|
+
content: [{ type: "text", text: "For 'Pull Request' links, 'projectId', 'repositoryId', and 'pullRequestId' are required." }],
|
|
742
|
+
isError: true,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
finalArtifactUri = `vstfs:///Git/PullRequestId/${encodeURIComponent(projectId)}%2F${encodeURIComponent(repositoryId)}%2F${encodeURIComponent(pullRequestId.toString())}`;
|
|
746
|
+
break;
|
|
747
|
+
case "Build":
|
|
748
|
+
case "Found in build":
|
|
749
|
+
case "Integrated in build":
|
|
750
|
+
if (buildId === undefined) {
|
|
751
|
+
return {
|
|
752
|
+
content: [{ type: "text", text: `For '${linkType}' links, 'buildId' is required.` }],
|
|
753
|
+
isError: true,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
finalArtifactUri = `vstfs:///Build/Build/${encodeURIComponent(buildId.toString())}`;
|
|
757
|
+
break;
|
|
758
|
+
default:
|
|
759
|
+
return {
|
|
760
|
+
content: [{ type: "text", text: `URI building from components is not supported for link type '${linkType}'. Please provide the full 'artifactUri' instead.` }],
|
|
761
|
+
isError: true,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// Create the patch document for adding an artifact link relation
|
|
766
|
+
const patchDocument = [
|
|
456
767
|
{
|
|
457
768
|
op: "add",
|
|
458
769
|
path: "/relations/-",
|
|
459
770
|
value: {
|
|
460
|
-
rel: "
|
|
461
|
-
url:
|
|
771
|
+
rel: "ArtifactLink",
|
|
772
|
+
url: finalArtifactUri,
|
|
773
|
+
attributes: {
|
|
774
|
+
name: linkType,
|
|
775
|
+
...(comment && { comment }),
|
|
776
|
+
},
|
|
462
777
|
},
|
|
463
778
|
},
|
|
464
|
-
]
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
779
|
+
];
|
|
780
|
+
// Use the WorkItem API to update the work item with the new relation
|
|
781
|
+
const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, project);
|
|
782
|
+
if (!workItem) {
|
|
783
|
+
return { content: [{ type: "text", text: "Work item update failed" }], isError: true };
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
content: [
|
|
787
|
+
{
|
|
788
|
+
type: "text",
|
|
789
|
+
text: JSON.stringify({
|
|
790
|
+
workItemId,
|
|
791
|
+
artifactUri: finalArtifactUri,
|
|
792
|
+
linkType,
|
|
793
|
+
comment: comment || null,
|
|
794
|
+
success: true,
|
|
795
|
+
}, null, 2),
|
|
796
|
+
},
|
|
797
|
+
],
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
802
|
+
return {
|
|
803
|
+
content: [{ type: "text", text: `Error adding artifact link to work item: ${errorMessage}` }],
|
|
804
|
+
isError: true,
|
|
805
|
+
};
|
|
478
806
|
}
|
|
479
|
-
const result = await response.json();
|
|
480
|
-
return {
|
|
481
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
482
|
-
};
|
|
483
807
|
});
|
|
484
808
|
}
|
|
485
809
|
export { WORKITEM_TOOLS, configureWorkItemTools };
|