@azure-devops/mcp 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/tools/repos.js +132 -8
- package/dist/tools/search.js +15 -15
- package/dist/tools/workitems.js +7 -58
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,12 +73,12 @@ Interact with these Azure DevOps services:
|
|
|
73
73
|
- **wit_get_query**: Get a query by its ID or path.
|
|
74
74
|
- **wit_get_query_results_by_id**: Retrieve the results of a work item query given the query ID.
|
|
75
75
|
- **wit_update_work_items_batch**: Update work items in batch.
|
|
76
|
-
- **wit_close_and_link_workitem_duplicates**: Close duplicate work items by ID.
|
|
77
76
|
- **wit_work_items_link**: Link work items together in batch.
|
|
78
77
|
|
|
79
78
|
#### Deprecated Tools
|
|
80
79
|
|
|
81
80
|
- **wit_add_child_work_item**: Replaced by `wit_add_child_work_items` to allow creating one or more child items per call.
|
|
81
|
+
- **wit_close_and_link_workitem_duplicates**: This tool is no longer needed. Finding and marking duplicates can be done with other tools.
|
|
82
82
|
|
|
83
83
|
### 📁 Repositories
|
|
84
84
|
|
|
@@ -96,9 +96,10 @@ Interact with these Azure DevOps services:
|
|
|
96
96
|
- **repo_create_pull_request**: Create a new pull request.
|
|
97
97
|
- **repo_update_pull_request_status**: Update the status of an existing pull request to active or abandoned.
|
|
98
98
|
- **repo_update_pull_request_reviewers**: Add or remove reviewers for an existing pull request.
|
|
99
|
-
- **repo_reply_to_comment**:
|
|
100
|
-
- **repo_resolve_comment**:
|
|
101
|
-
- **repo_search_commits**:
|
|
99
|
+
- **repo_reply_to_comment**: Replies to a specific comment on a pull request.
|
|
100
|
+
- **repo_resolve_comment**: Resolves a specific comment thread on a pull request.
|
|
101
|
+
- **repo_search_commits**: Searches for commits.
|
|
102
|
+
- **repo_create_pull_request_thread**: Creates a new comment thread on a pull request.
|
|
102
103
|
|
|
103
104
|
### 🛰️ Builds
|
|
104
105
|
|
package/dist/tools/repos.js
CHANGED
|
@@ -19,6 +19,7 @@ const REPO_TOOLS = {
|
|
|
19
19
|
update_pull_request_status: "repo_update_pull_request_status",
|
|
20
20
|
update_pull_request_reviewers: "repo_update_pull_request_reviewers",
|
|
21
21
|
reply_to_comment: "repo_reply_to_comment",
|
|
22
|
+
create_pull_request_thread: "repo_create_pull_request_thread",
|
|
22
23
|
resolve_comment: "repo_resolve_comment",
|
|
23
24
|
search_commits: "repo_search_commits",
|
|
24
25
|
list_pull_requests_by_commits: "repo_list_pull_requests_by_commits",
|
|
@@ -31,6 +32,26 @@ function branchesFilterOutIrrelevantProperties(branches, top) {
|
|
|
31
32
|
.sort((a, b) => b.localeCompare(a))
|
|
32
33
|
.slice(0, top);
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Trims comment data to essential properties, filtering out deleted comments
|
|
37
|
+
* @param comments Array of comments to trim (can be undefined/null)
|
|
38
|
+
* @returns Array of trimmed comment objects with essential properties only
|
|
39
|
+
*/
|
|
40
|
+
function trimComments(comments) {
|
|
41
|
+
return comments
|
|
42
|
+
?.filter((comment) => !comment.isDeleted) // Exclude deleted comments
|
|
43
|
+
?.map((comment) => ({
|
|
44
|
+
id: comment.id,
|
|
45
|
+
author: {
|
|
46
|
+
displayName: comment.author?.displayName,
|
|
47
|
+
uniqueName: comment.author?.uniqueName,
|
|
48
|
+
},
|
|
49
|
+
content: comment.content,
|
|
50
|
+
publishedDate: comment.publishedDate,
|
|
51
|
+
lastUpdatedDate: comment.lastUpdatedDate,
|
|
52
|
+
lastContentUpdatedDate: comment.lastContentUpdatedDate,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
34
55
|
function pullRequestStatusStringToInt(status) {
|
|
35
56
|
switch (status) {
|
|
36
57
|
case "Abandoned":
|
|
@@ -242,13 +263,27 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
242
263
|
baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
|
|
243
264
|
top: z.number().default(100).describe("The maximum number of threads to return."),
|
|
244
265
|
skip: z.number().default(0).describe("The number of threads to skip."),
|
|
245
|
-
|
|
266
|
+
fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of trimmed data."),
|
|
267
|
+
}, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, top, skip, fullResponse }) => {
|
|
246
268
|
const connection = await connectionProvider();
|
|
247
269
|
const gitApi = await connection.getGitApi();
|
|
248
270
|
const threads = await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration);
|
|
249
271
|
const paginatedThreads = threads?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
|
|
272
|
+
if (fullResponse) {
|
|
273
|
+
return {
|
|
274
|
+
content: [{ type: "text", text: JSON.stringify(paginatedThreads, null, 2) }],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// Return trimmed thread data focusing on essential information
|
|
278
|
+
const trimmedThreads = paginatedThreads?.map((thread) => ({
|
|
279
|
+
id: thread.id,
|
|
280
|
+
publishedDate: thread.publishedDate,
|
|
281
|
+
lastUpdatedDate: thread.lastUpdatedDate,
|
|
282
|
+
status: thread.status,
|
|
283
|
+
comments: trimComments(thread.comments),
|
|
284
|
+
}));
|
|
250
285
|
return {
|
|
251
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
286
|
+
content: [{ type: "text", text: JSON.stringify(trimmedThreads, null, 2) }],
|
|
252
287
|
};
|
|
253
288
|
});
|
|
254
289
|
server.tool(REPO_TOOLS.list_pull_request_thread_comments, "Retrieve a list of comments in a pull request thread.", {
|
|
@@ -258,14 +293,22 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
258
293
|
project: z.string().optional().describe("Project ID or project name (optional)"),
|
|
259
294
|
top: z.number().default(100).describe("The maximum number of comments to return."),
|
|
260
295
|
skip: z.number().default(0).describe("The number of comments to skip."),
|
|
261
|
-
|
|
296
|
+
fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of trimmed data."),
|
|
297
|
+
}, async ({ repositoryId, pullRequestId, threadId, project, top, skip, fullResponse }) => {
|
|
262
298
|
const connection = await connectionProvider();
|
|
263
299
|
const gitApi = await connection.getGitApi();
|
|
264
300
|
// Get thread comments - GitApi uses getComments for retrieving comments from a specific thread
|
|
265
301
|
const comments = await gitApi.getComments(repositoryId, pullRequestId, threadId, project);
|
|
266
302
|
const paginatedComments = comments?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
|
|
303
|
+
if (fullResponse) {
|
|
304
|
+
return {
|
|
305
|
+
content: [{ type: "text", text: JSON.stringify(paginatedComments, null, 2) }],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
// Return trimmed comment data focusing on essential information
|
|
309
|
+
const trimmedComments = trimComments(paginatedComments);
|
|
267
310
|
return {
|
|
268
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
311
|
+
content: [{ type: "text", text: JSON.stringify(trimmedComments, null, 2) }],
|
|
269
312
|
};
|
|
270
313
|
});
|
|
271
314
|
server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
|
|
@@ -346,25 +389,106 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
346
389
|
threadId: z.number().describe("The ID of the thread to which the comment will be added."),
|
|
347
390
|
content: z.string().describe("The content of the comment to be added."),
|
|
348
391
|
project: z.string().optional().describe("Project ID or project name (optional)"),
|
|
349
|
-
|
|
392
|
+
fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of a simple confirmation message."),
|
|
393
|
+
}, async ({ repositoryId, pullRequestId, threadId, content, project, fullResponse }) => {
|
|
350
394
|
const connection = await connectionProvider();
|
|
351
395
|
const gitApi = await connection.getGitApi();
|
|
352
396
|
const comment = await gitApi.createComment({ content }, repositoryId, pullRequestId, threadId, project);
|
|
397
|
+
// Check if the comment was successfully created
|
|
398
|
+
if (!comment) {
|
|
399
|
+
return {
|
|
400
|
+
content: [{ type: "text", text: `Error: Failed to add comment to thread ${threadId}. The comment was not created successfully.` }],
|
|
401
|
+
isError: true,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (fullResponse) {
|
|
405
|
+
return {
|
|
406
|
+
content: [{ type: "text", text: JSON.stringify(comment, null, 2) }],
|
|
407
|
+
};
|
|
408
|
+
}
|
|
353
409
|
return {
|
|
354
|
-
content: [{ type: "text", text:
|
|
410
|
+
content: [{ type: "text", text: `Comment successfully added to thread ${threadId}.` }],
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
server.tool(REPO_TOOLS.create_pull_request_thread, "Creates a new comment thread on a pull request.", {
|
|
414
|
+
repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
|
|
415
|
+
pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
|
|
416
|
+
content: z.string().describe("The content of the comment to be added."),
|
|
417
|
+
project: z.string().optional().describe("Project ID or project name (optional)"),
|
|
418
|
+
filePath: z.string().optional().describe("The path of the file where the comment thread will be created. (optional)"),
|
|
419
|
+
rightFileStartLine: z.number().optional().describe("Position of first character of the thread's span in right file. The line number of a thread's position. Starts at 1. (optional)"),
|
|
420
|
+
rightFileStartOffset: z
|
|
421
|
+
.number()
|
|
422
|
+
.optional()
|
|
423
|
+
.describe("Position of first character of the thread's span in right file. The line number of a thread's position. The character offset of a thread's position inside of a line. Starts at 1. Must only be set if rightFileStartLine is also specified. (optional)"),
|
|
424
|
+
rightFileEndLine: z
|
|
425
|
+
.number()
|
|
426
|
+
.optional()
|
|
427
|
+
.describe("Position of last character of the thread's span in right file. The line number of a thread's position. Starts at 1. Must only be set if rightFileStartLine is also specified. (optional)"),
|
|
428
|
+
rightFileEndOffset: z
|
|
429
|
+
.number()
|
|
430
|
+
.optional()
|
|
431
|
+
.describe("Position of last character of the thread's span in right file. The character offset of a thread's position inside of a line. Must only be set if rightFileEndLine is also specified. (optional)"),
|
|
432
|
+
}, async ({ repositoryId, pullRequestId, content, project, filePath, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset }) => {
|
|
433
|
+
const connection = await connectionProvider();
|
|
434
|
+
const gitApi = await connection.getGitApi();
|
|
435
|
+
const threadContext = { filePath: filePath };
|
|
436
|
+
if (rightFileStartLine !== undefined) {
|
|
437
|
+
if (rightFileStartLine < 1) {
|
|
438
|
+
throw new Error("rightFileStartLine must be greater than or equal to 1.");
|
|
439
|
+
}
|
|
440
|
+
threadContext.rightFileStart = { line: rightFileStartLine };
|
|
441
|
+
if (rightFileStartOffset !== undefined) {
|
|
442
|
+
if (rightFileStartOffset < 1) {
|
|
443
|
+
throw new Error("rightFileStartOffset must be greater than or equal to 1.");
|
|
444
|
+
}
|
|
445
|
+
threadContext.rightFileStart.offset = rightFileStartOffset;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (rightFileEndLine !== undefined) {
|
|
449
|
+
if (rightFileStartLine === undefined) {
|
|
450
|
+
throw new Error("rightFileEndLine must only be specified if rightFileStartLine is also specified.");
|
|
451
|
+
}
|
|
452
|
+
if (rightFileEndLine < 1) {
|
|
453
|
+
throw new Error("rightFileEndLine must be greater than or equal to 1.");
|
|
454
|
+
}
|
|
455
|
+
threadContext.rightFileEnd = { line: rightFileEndLine };
|
|
456
|
+
if (rightFileEndOffset !== undefined) {
|
|
457
|
+
if (rightFileEndOffset < 1) {
|
|
458
|
+
throw new Error("rightFileEndOffset must be greater than or equal to 1.");
|
|
459
|
+
}
|
|
460
|
+
threadContext.rightFileEnd.offset = rightFileEndOffset;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const thread = await gitApi.createThread({ comments: [{ content: content }], threadContext: threadContext }, repositoryId, pullRequestId, project);
|
|
464
|
+
return {
|
|
465
|
+
content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
|
|
355
466
|
};
|
|
356
467
|
});
|
|
357
468
|
server.tool(REPO_TOOLS.resolve_comment, "Resolves a specific comment thread on a pull request.", {
|
|
358
469
|
repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
|
|
359
470
|
pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
|
|
360
471
|
threadId: z.number().describe("The ID of the thread to be resolved."),
|
|
361
|
-
|
|
472
|
+
fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of a simple confirmation message."),
|
|
473
|
+
}, async ({ repositoryId, pullRequestId, threadId, fullResponse }) => {
|
|
362
474
|
const connection = await connectionProvider();
|
|
363
475
|
const gitApi = await connection.getGitApi();
|
|
364
476
|
const thread = await gitApi.updateThread({ status: 2 }, // 2 corresponds to "Resolved" status
|
|
365
477
|
repositoryId, pullRequestId, threadId);
|
|
478
|
+
// Check if the thread was successfully resolved
|
|
479
|
+
if (!thread) {
|
|
480
|
+
return {
|
|
481
|
+
content: [{ type: "text", text: `Error: Failed to resolve thread ${threadId}. The thread status was not updated successfully.` }],
|
|
482
|
+
isError: true,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (fullResponse) {
|
|
486
|
+
return {
|
|
487
|
+
content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
|
|
488
|
+
};
|
|
489
|
+
}
|
|
366
490
|
return {
|
|
367
|
-
content: [{ type: "text", text:
|
|
491
|
+
content: [{ type: "text", text: `Thread ${threadId} was successfully resolved.` }],
|
|
368
492
|
};
|
|
369
493
|
});
|
|
370
494
|
const gitVersionTypeStrings = Object.values(GitVersionType).filter((value) => typeof value === "string");
|
package/dist/tools/search.js
CHANGED
|
@@ -17,17 +17,17 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
17
17
|
path: z.array(z.string()).optional().describe("Filter by paths"),
|
|
18
18
|
branch: z.array(z.string()).optional().describe("Filter by branches"),
|
|
19
19
|
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}, async ({ searchText, project, repository, path, branch, includeFacets,
|
|
20
|
+
skip: z.number().default(0).describe("Number of results to skip"),
|
|
21
|
+
top: z.number().default(5).describe("Maximum number of results to return"),
|
|
22
|
+
}, async ({ searchText, project, repository, path, branch, includeFacets, skip, top }) => {
|
|
23
23
|
const accessToken = await tokenProvider();
|
|
24
24
|
const connection = await connectionProvider();
|
|
25
25
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/codesearchresults?api-version=${apiVersion}`;
|
|
26
26
|
const requestBody = {
|
|
27
27
|
searchText,
|
|
28
28
|
includeFacets,
|
|
29
|
-
$skip,
|
|
30
|
-
$top,
|
|
29
|
+
$skip: skip,
|
|
30
|
+
$top: top,
|
|
31
31
|
};
|
|
32
32
|
const filters = {};
|
|
33
33
|
if (project && project.length > 0)
|
|
@@ -66,16 +66,16 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
66
66
|
project: z.array(z.string()).optional().describe("Filter by projects"),
|
|
67
67
|
wiki: z.array(z.string()).optional().describe("Filter by wiki names"),
|
|
68
68
|
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}, async ({ searchText, project, wiki, includeFacets,
|
|
69
|
+
skip: z.number().default(0).describe("Number of results to skip"),
|
|
70
|
+
top: z.number().default(10).describe("Maximum number of results to return"),
|
|
71
|
+
}, async ({ searchText, project, wiki, includeFacets, skip, top }) => {
|
|
72
72
|
const accessToken = await tokenProvider();
|
|
73
73
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/wikisearchresults?api-version=${apiVersion}`;
|
|
74
74
|
const requestBody = {
|
|
75
75
|
searchText,
|
|
76
76
|
includeFacets,
|
|
77
|
-
$skip,
|
|
78
|
-
$top,
|
|
77
|
+
$skip: skip,
|
|
78
|
+
$top: top,
|
|
79
79
|
};
|
|
80
80
|
const filters = {};
|
|
81
81
|
if (project && project.length > 0)
|
|
@@ -110,16 +110,16 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
110
110
|
state: z.array(z.string()).optional().describe("Filter by work item states"),
|
|
111
111
|
assignedTo: z.array(z.string()).optional().describe("Filter by assigned to users"),
|
|
112
112
|
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}, async ({ searchText, project, areaPath, workItemType, state, assignedTo, includeFacets,
|
|
113
|
+
skip: z.number().default(0).describe("Number of results to skip for pagination"),
|
|
114
|
+
top: z.number().default(10).describe("Number of results to return"),
|
|
115
|
+
}, async ({ searchText, project, areaPath, workItemType, state, assignedTo, includeFacets, skip, top }) => {
|
|
116
116
|
const accessToken = await tokenProvider();
|
|
117
117
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/workitemsearchresults?api-version=${apiVersion}`;
|
|
118
118
|
const requestBody = {
|
|
119
119
|
searchText,
|
|
120
120
|
includeFacets,
|
|
121
|
-
$skip,
|
|
122
|
-
$top,
|
|
121
|
+
$skip: skip,
|
|
122
|
+
$top: top,
|
|
123
123
|
};
|
|
124
124
|
const filters = {};
|
|
125
125
|
if (project && project.length > 0)
|
package/dist/tools/workitems.js
CHANGED
|
@@ -3,14 +3,6 @@
|
|
|
3
3
|
import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { batchApiVersion, markdownCommentsApiVersion, getEnumKeys, safeEnumConvert } from "../utils.js";
|
|
6
|
-
/**
|
|
7
|
-
* Converts Operation enum key to lowercase string for API usage
|
|
8
|
-
* @param operation The Operation enum key (e.g., "Add", "Replace", "Remove")
|
|
9
|
-
* @returns Lowercase string for API usage (e.g., "add", "replace", "remove")
|
|
10
|
-
*/
|
|
11
|
-
function operationToApiString(operation) {
|
|
12
|
-
return operation.toLowerCase();
|
|
13
|
-
}
|
|
14
6
|
const WORKITEM_TOOLS = {
|
|
15
7
|
my_work_items: "wit_my_work_items",
|
|
16
8
|
list_backlogs: "wit_list_backlogs",
|
|
@@ -28,7 +20,6 @@ const WORKITEM_TOOLS = {
|
|
|
28
20
|
get_query: "wit_get_query",
|
|
29
21
|
get_query_results_by_id: "wit_get_query_results_by_id",
|
|
30
22
|
update_work_items_batch: "wit_update_work_items_batch",
|
|
31
|
-
close_and_link_workitem_duplicates: "wit_close_and_link_workitem_duplicates",
|
|
32
23
|
work_items_link: "wit_work_items_link",
|
|
33
24
|
};
|
|
34
25
|
function getLinkTypeFromName(name) {
|
|
@@ -353,7 +344,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
353
344
|
id: z.number().describe("The ID of the work item to update."),
|
|
354
345
|
updates: z
|
|
355
346
|
.array(z.object({
|
|
356
|
-
op: z
|
|
347
|
+
op: z
|
|
348
|
+
.string()
|
|
349
|
+
.transform((val) => val.toLowerCase())
|
|
350
|
+
.pipe(z.enum(["add", "replace", "remove"]))
|
|
351
|
+
.default("add")
|
|
352
|
+
.describe("The operation to perform on the field."),
|
|
357
353
|
path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
|
|
358
354
|
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."),
|
|
359
355
|
}))
|
|
@@ -364,7 +360,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
364
360
|
// Convert operation names to lowercase for API
|
|
365
361
|
const apiUpdates = updates.map((update) => ({
|
|
366
362
|
...update,
|
|
367
|
-
op:
|
|
363
|
+
op: update.op,
|
|
368
364
|
}));
|
|
369
365
|
const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id);
|
|
370
366
|
return {
|
|
@@ -577,52 +573,5 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
577
573
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
578
574
|
};
|
|
579
575
|
});
|
|
580
|
-
server.tool(WORKITEM_TOOLS.close_and_link_workitem_duplicates, "Close duplicate work items by id.", {
|
|
581
|
-
id: z.number().describe("The ID of the work item to close and link duplicates to."),
|
|
582
|
-
duplicateIds: z.array(z.number()).describe("An array of IDs of the duplicate work items to close and link to the specified work item."),
|
|
583
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
584
|
-
state: z.string().default("Removed").describe("The state to set for the duplicate work items. Defaults to 'Removed'."),
|
|
585
|
-
}, async ({ id, duplicateIds, project, state }) => {
|
|
586
|
-
const connection = await connectionProvider();
|
|
587
|
-
const body = duplicateIds.map((duplicateId) => ({
|
|
588
|
-
method: "PATCH",
|
|
589
|
-
uri: `/_apis/wit/workitems/${duplicateId}?api-version=${batchApiVersion}`,
|
|
590
|
-
headers: {
|
|
591
|
-
"Content-Type": "application/json-patch+json",
|
|
592
|
-
},
|
|
593
|
-
body: [
|
|
594
|
-
{
|
|
595
|
-
op: "add",
|
|
596
|
-
path: "/fields/System.State",
|
|
597
|
-
value: `${state}`,
|
|
598
|
-
},
|
|
599
|
-
{
|
|
600
|
-
op: "add",
|
|
601
|
-
path: "/relations/-",
|
|
602
|
-
value: {
|
|
603
|
-
rel: "System.LinkTypes.Duplicate-Reverse",
|
|
604
|
-
url: `${connection.serverUrl}/${project}/_apis/wit/workItems/${id}`,
|
|
605
|
-
},
|
|
606
|
-
},
|
|
607
|
-
],
|
|
608
|
-
}));
|
|
609
|
-
const accessToken = await tokenProvider();
|
|
610
|
-
const response = await fetch(`${connection.serverUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
|
|
611
|
-
method: "PATCH",
|
|
612
|
-
headers: {
|
|
613
|
-
"Authorization": `Bearer ${accessToken.token}`,
|
|
614
|
-
"Content-Type": "application/json",
|
|
615
|
-
"User-Agent": userAgentProvider(),
|
|
616
|
-
},
|
|
617
|
-
body: JSON.stringify(body),
|
|
618
|
-
});
|
|
619
|
-
if (!response.ok) {
|
|
620
|
-
throw new Error(`Failed to update work items in batch: ${response.statusText}`);
|
|
621
|
-
}
|
|
622
|
-
const result = await response.json();
|
|
623
|
-
return {
|
|
624
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
625
|
-
};
|
|
626
|
-
});
|
|
627
576
|
}
|
|
628
577
|
export { WORKITEM_TOOLS, configureWorkItemTools };
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "1.2.
|
|
1
|
+
export const packageVersion = "1.2.1";
|