@azure-devops/mcp 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -25
- package/dist/auth.js +2 -103
- package/dist/oauth.js +130 -0
- package/dist/shared/tool-annotations.js +85 -0
- package/dist/tools/pipelines.js +51 -12
- package/dist/tools/repositories.js +208 -26
- package/dist/tools/search.js +26 -3
- package/dist/tools/test-plans.js +55 -18
- package/dist/tools/wiki.js +9 -9
- package/dist/tools/work-items.js +149 -31
- package/dist/tools.js +20 -17
- package/dist/version.js +1 -1
- package/package.json +5 -3
package/dist/tools/work-items.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
-
import
|
|
3
|
+
import { constants, open, realpath } from "fs/promises";
|
|
4
4
|
import * as path from "path";
|
|
5
|
+
import { Readable } from "stream";
|
|
5
6
|
import { WorkItemExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
6
7
|
import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
7
8
|
import { z } from "zod";
|
|
@@ -14,10 +15,25 @@ const WORKITEM_TOOLS = {
|
|
|
14
15
|
wit_query: "wit_query",
|
|
15
16
|
wit_backlog: "wit_backlog",
|
|
16
17
|
wit_work_item_attachment: "wit_work_item_attachment",
|
|
18
|
+
wit_work_item_attachment_upload: "wit_work_item_attachment_upload",
|
|
19
|
+
wit_work_item_attachment_link: "wit_work_item_attachment_link",
|
|
17
20
|
wit_work_item_write: "wit_work_item_write",
|
|
18
21
|
wit_work_item_comment_write: "wit_work_item_comment_write",
|
|
19
22
|
wit_work_item_link_write: "wit_work_item_link_write",
|
|
20
23
|
};
|
|
24
|
+
const jsonPatchValueSchema = z.lazy(() => z.union([z.string(), z.number().finite(), z.boolean(), z.null(), z.array(jsonPatchValueSchema), z.record(jsonPatchValueSchema)]));
|
|
25
|
+
const readConsistencyHandler = {
|
|
26
|
+
prepareRequest(options) {
|
|
27
|
+
options.headers = { ...options.headers, "X-VSS-ReadConsistencyLevel": "Eventual" };
|
|
28
|
+
},
|
|
29
|
+
canHandleAuthentication: () => false,
|
|
30
|
+
async handleAuthentication() {
|
|
31
|
+
throw new Error("The read-consistency handler does not handle authentication.");
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
function getReadReplicaWorkItemTrackingApi(connection) {
|
|
35
|
+
return connection.getWorkItemTrackingApi(undefined, [connection.authHandler, readConsistencyHandler]);
|
|
36
|
+
}
|
|
21
37
|
function getLinkTypeFromName(name) {
|
|
22
38
|
switch (name.toLowerCase()) {
|
|
23
39
|
case "parent":
|
|
@@ -68,10 +84,8 @@ function escapeHtml(value) {
|
|
|
68
84
|
};
|
|
69
85
|
return value.replace(/[&<>"']/g, (character) => entities[character]);
|
|
70
86
|
}
|
|
71
|
-
async function
|
|
87
|
+
async function prepareCommentText(text, format, tokenProvider, connectionProvider, userAgentProvider) {
|
|
72
88
|
const emailMatches = [...text.matchAll(/@<([^<>\s]+@[^<>\s]+)>/g)];
|
|
73
|
-
if (emailMatches.length === 0)
|
|
74
|
-
return text;
|
|
75
89
|
const identities = new Map();
|
|
76
90
|
for (const email of new Set(emailMatches.map((match) => match[1]))) {
|
|
77
91
|
try {
|
|
@@ -81,20 +95,30 @@ async function resolveCommentMentions(text, format, tokenProvider, connectionPro
|
|
|
81
95
|
// Leave mentions unchanged when their identities cannot be resolved.
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
|
-
|
|
98
|
+
// Encode Markdown transport text once, preserving mention tokens for Azure DevOps.
|
|
99
|
+
// Encoding ampersands also preserves intentional entity text after one decode.
|
|
100
|
+
const markdown = format === "Markdown" || format === undefined;
|
|
101
|
+
return text.replace(/@<([^<>\s]+@[^<>\s]+)>|@<([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})>|[&<>]/gi, (mention, email, id) => {
|
|
102
|
+
if (id)
|
|
103
|
+
return mention;
|
|
104
|
+
if (!email)
|
|
105
|
+
return markdown ? escapeHtml(mention) : mention;
|
|
85
106
|
const identity = identities.get(email);
|
|
86
107
|
if (!identity)
|
|
87
108
|
return escapeHtml(mention);
|
|
88
|
-
return
|
|
109
|
+
return markdown ? `@<${identity.id}>` : `<a href="#" data-vss-mention="version:2.0,${identity.id}">@${escapeHtml(identity.displayName)}</a>`;
|
|
89
110
|
});
|
|
90
111
|
}
|
|
91
112
|
function configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
92
113
|
// --- wit_work_item ----------------------------------------------------------
|
|
93
|
-
server.tool(WORKITEM_TOOLS.wit_work_item, "Retrieve work item data
|
|
114
|
+
server.tool(WORKITEM_TOOLS.wit_work_item, "Retrieve work item data. Use the action parameter to specify the operation.", {
|
|
94
115
|
action: z
|
|
95
116
|
.enum(["get", "get_batch", "list_comments", "my", "list_revisions", "list_for_iteration", "get_type"])
|
|
96
117
|
.describe("The action to perform. Options: get (get a single work item by ID), get_batch (get multiple work items by IDs), list_comments (list comments on a work item), my (get work items relevant to the authenticated user), list_revisions (list revisions of a work item), list_for_iteration (list work items for a team iteration), get_type (get metadata for a work item type)."),
|
|
97
|
-
project: z
|
|
118
|
+
project: z
|
|
119
|
+
.string()
|
|
120
|
+
.optional()
|
|
121
|
+
.describe("The name or ID of the Azure DevOps project. Optional for get; when omitted, the work item is retrieved at organization scope. For other actions, a project selection prompt will be shown if omitted."),
|
|
98
122
|
id: z.coerce.number().min(1).optional().describe("Work item ID. Required for: get."),
|
|
99
123
|
ids: z.array(z.coerce.number().min(1)).optional().describe("Work item IDs. Required for: get_batch."),
|
|
100
124
|
workItemId: z.coerce.number().min(1).optional().describe("Work item ID. Required for: list_comments, list_revisions."),
|
|
@@ -105,25 +129,20 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
105
129
|
.optional()
|
|
106
130
|
.describe("Expand options (None, Fields, Relations, Links, All). Used for: get, list_revisions. For get, cannot be combined with fields."),
|
|
107
131
|
top: z.coerce.number().optional().describe("Maximum number of results to return. Used for: list_comments, my, list_revisions. Defaults vary by action."),
|
|
132
|
+
continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list_comments."),
|
|
108
133
|
includeCompleted: z.boolean().optional().default(false).describe("Include completed work items. Used for: my. Defaults to false."),
|
|
109
134
|
type: z.enum(["assignedtome", "myactivity"]).optional().describe("Type of work items to retrieve. Used for: my. Defaults to 'assignedtome'."),
|
|
110
135
|
skip: z.coerce.number().optional().describe("Number of results to skip for pagination. Used for: list_revisions."),
|
|
111
136
|
team: z.string().optional().describe("Team name or ID. Used for: list_for_iteration."),
|
|
112
137
|
iterationId: z.string().optional().describe("Iteration ID. Required for: list_for_iteration."),
|
|
113
138
|
workItemType: z.string().optional().describe("Work item type name. Required for: get_type."),
|
|
114
|
-
}, async ({ action, project, id, ids, workItemId, fields, asOf, expand, top, includeCompleted, type, skip, team, iterationId, workItemType }) => {
|
|
139
|
+
}, async ({ action, project, id, ids, workItemId, fields, asOf, expand, top, continuationToken, includeCompleted, type, skip, team, iterationId, workItemType }) => {
|
|
115
140
|
try {
|
|
116
141
|
const connection = await connectionProvider();
|
|
117
142
|
let resolvedProject = project;
|
|
118
143
|
if (action === "get") {
|
|
119
144
|
if (!id)
|
|
120
145
|
return { content: [{ type: "text", text: "id is required for get" }], isError: true };
|
|
121
|
-
if (!resolvedProject) {
|
|
122
|
-
const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the work item from.");
|
|
123
|
-
if ("response" in result)
|
|
124
|
-
return result.response;
|
|
125
|
-
resolvedProject = result.resolved;
|
|
126
|
-
}
|
|
127
146
|
let effectiveExpand = expand;
|
|
128
147
|
if (fields && fields.length > 0 && effectiveExpand != null) {
|
|
129
148
|
effectiveExpand = "none";
|
|
@@ -180,7 +199,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
180
199
|
resolvedProject = result.resolved;
|
|
181
200
|
}
|
|
182
201
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
183
|
-
const comments = await workItemApi.getComments(resolvedProject, workItemId, top ?? 50);
|
|
202
|
+
const comments = await workItemApi.getComments(resolvedProject, workItemId, top ?? 50, continuationToken);
|
|
184
203
|
return { content: [{ type: "text", text: JSON.stringify(comments, null, 2) }] };
|
|
185
204
|
}
|
|
186
205
|
if (action === "my") {
|
|
@@ -289,7 +308,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
289
308
|
team: z.string().optional().describe("Team name or ID. Used for: get_results, wiql."),
|
|
290
309
|
timePrecision: z.boolean().optional().describe("Include time precision in date fields. Used for: get_results, wiql."),
|
|
291
310
|
top: z.coerce.number().default(50).describe("Maximum number of results to return. Used for: get_results, wiql. Defaults to 50."),
|
|
292
|
-
responseType: z
|
|
311
|
+
responseType: z
|
|
312
|
+
.enum(["full", "ids"])
|
|
313
|
+
.default("full")
|
|
314
|
+
.describe("Response type: 'full' returns complete results (default). 'ids' returns only work item IDs and supports flat queries only; any other query type is not supported. Used for: get_results."),
|
|
293
315
|
wiql: z.string().max(32768).optional().describe('The WIQL query string to execute. Required for: wiql. Example: "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @project".'),
|
|
294
316
|
}, async ({ action, project, query, expand, depth, includeDeleted, useIsoDateFormat, id, team, timePrecision, top, responseType, wiql }) => {
|
|
295
317
|
try {
|
|
@@ -311,7 +333,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
311
333
|
if (action === "get_results") {
|
|
312
334
|
if (!id)
|
|
313
335
|
return { content: [{ type: "text", text: "id is required for get_results" }], isError: true };
|
|
314
|
-
const workItemApi = await connection
|
|
336
|
+
const workItemApi = await getReadReplicaWorkItemTrackingApi(connection);
|
|
315
337
|
const teamContext = { project: resolvedProject, team };
|
|
316
338
|
const queryResult = await workItemApi.queryById(id, teamContext, timePrecision, top);
|
|
317
339
|
if (responseType === "ids") {
|
|
@@ -323,7 +345,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
323
345
|
if (action === "wiql") {
|
|
324
346
|
if (!wiql)
|
|
325
347
|
return { content: [{ type: "text", text: "wiql is required for wiql" }], isError: true };
|
|
326
|
-
const workItemApi = await connection
|
|
348
|
+
const workItemApi = await getReadReplicaWorkItemTrackingApi(connection);
|
|
327
349
|
const teamContext = { project: resolvedProject, team };
|
|
328
350
|
const queryResult = await workItemApi.queryByWiql({ query: wiql }, teamContext, timePrecision, top);
|
|
329
351
|
return createExternalContentResponse(queryResult, "wiql query results");
|
|
@@ -412,12 +434,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
412
434
|
// --- wit_work_item_attachment -----------------------------------------------
|
|
413
435
|
server.tool(WORKITEM_TOOLS.wit_work_item_attachment, "Download a work item attachment by its ID. By default returns the content as a base64-encoded resource. If savePath is provided, saves the file locally to that directory and returns the file path instead. Useful for viewing images (e.g. screenshots) or other files attached to work items such as bugs. If a project is not specified, you will be prompted to select one.", {
|
|
414
436
|
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."),
|
|
415
|
-
attachmentId: z.string().describe("The GUID of the attachment. Found in the attachment URL: https://dev.azure.com/{org}/{project}/_apis/wit/attachments/{attachmentId}"),
|
|
437
|
+
attachmentId: z.string().describe("The GUID of the attachment. Found in the attachment URL: https://dev.azure.com/{org}/{project}/_apis/wit/attachments/{attachmentId}."),
|
|
416
438
|
fileName: z.string().optional().describe("The file name of the attachment, e.g. 'screenshot.png'. Used to determine the MIME type or the saved file's name."),
|
|
417
439
|
savePath: z
|
|
418
440
|
.string()
|
|
419
441
|
.optional()
|
|
420
|
-
.describe("Optional local directory path where the file should be saved. Must be a relative path (e.g. 'temp' or 'downloads/attachments'); absolute paths
|
|
442
|
+
.describe("Optional local directory path where the downloaded file should be saved. Must be a relative path (e.g. 'temp' or 'downloads/attachments') to an existing directory that resolves (including symlinks) inside the server's working directory; absolute paths are not allowed. If provided, saves the attachment to this directory and returns the file path. If omitted, returns the content as a base64-encoded resource."),
|
|
421
443
|
}, async ({ project, attachmentId, fileName, savePath }) => {
|
|
422
444
|
const isAbsolutePath = (value) => path.posix.isAbsolute(value) || path.win32.isAbsolute(value);
|
|
423
445
|
const hasDriveLetter = (value) => /^[a-zA-Z]:/.test(value);
|
|
@@ -427,6 +449,14 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
427
449
|
if (fileName !== undefined && fileName.includes("..")) {
|
|
428
450
|
throw new Error("Invalid fileName: path traversal is not allowed.");
|
|
429
451
|
}
|
|
452
|
+
if (!attachmentId)
|
|
453
|
+
return { content: [{ type: "text", text: "attachmentId is required for download" }], isError: true };
|
|
454
|
+
if (savePath) {
|
|
455
|
+
const localFileName = fileName ?? attachmentId;
|
|
456
|
+
if (!localFileName || localFileName.includes("\0") || path.posix.basename(localFileName) !== localFileName || path.win32.basename(localFileName) !== localFileName) {
|
|
457
|
+
throw new Error("Invalid fileName: path components are not allowed.");
|
|
458
|
+
}
|
|
459
|
+
}
|
|
430
460
|
try {
|
|
431
461
|
const connection = await connectionProvider();
|
|
432
462
|
let resolvedProject = project;
|
|
@@ -438,6 +468,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
438
468
|
}
|
|
439
469
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
440
470
|
const stream = await workItemApi.getAttachmentContent(attachmentId, fileName, resolvedProject);
|
|
471
|
+
const responseStream = stream;
|
|
472
|
+
if (responseStream.statusCode !== undefined && (responseStream.statusCode < 200 || responseStream.statusCode >= 300)) {
|
|
473
|
+
responseStream.resume();
|
|
474
|
+
const statusMessage = responseStream.statusMessage ? ` ${responseStream.statusMessage}` : "";
|
|
475
|
+
throw new Error(`Attachment download failed: HTTP ${responseStream.statusCode}${statusMessage}`);
|
|
476
|
+
}
|
|
441
477
|
const chunks = [];
|
|
442
478
|
await new Promise((resolve, reject) => {
|
|
443
479
|
stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
@@ -446,12 +482,21 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
446
482
|
});
|
|
447
483
|
const buffer = Buffer.concat(chunks);
|
|
448
484
|
if (savePath) {
|
|
449
|
-
const
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
485
|
+
const workspaceRoot = await realpath(process.cwd());
|
|
486
|
+
const resolvedDirectory = await realpath(path.resolve(workspaceRoot, savePath));
|
|
487
|
+
const relativeDirectory = path.relative(workspaceRoot, resolvedDirectory);
|
|
488
|
+
if (relativeDirectory === ".." || relativeDirectory.startsWith(`..${path.sep}`) || path.isAbsolute(relativeDirectory)) {
|
|
489
|
+
throw new Error("Invalid savePath: destination must remain inside the workspace.");
|
|
490
|
+
}
|
|
491
|
+
const localFilePath = path.resolve(resolvedDirectory, fileName ?? attachmentId);
|
|
492
|
+
// O_EXCL fails if the file (or a symlink) already exists, so there is no check-then-write race.
|
|
493
|
+
const file = await open(localFilePath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
494
|
+
try {
|
|
495
|
+
await file.writeFile(buffer);
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
await file.close();
|
|
453
499
|
}
|
|
454
|
-
fs.writeFileSync(localFilePath, buffer);
|
|
455
500
|
return {
|
|
456
501
|
content: [{ type: "text", text: `Attachment saved to: ${localFilePath}` }],
|
|
457
502
|
};
|
|
@@ -482,6 +527,80 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
482
527
|
};
|
|
483
528
|
}
|
|
484
529
|
});
|
|
530
|
+
// --- wit_work_item_attachment_upload ----------------------------------------
|
|
531
|
+
server.tool(WORKITEM_TOOLS.wit_work_item_attachment_upload, "Upload base64-encoded content as a new work item attachment. This tool only uploads the attachment; use wit_work_item_attachment_link separately to link the returned attachment URL to a work item.", {
|
|
532
|
+
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."),
|
|
533
|
+
fileName: z.string().describe("The name of the file to upload, e.g. 'screenshot.png'."),
|
|
534
|
+
content: z.string().describe("Base64-encoded file content to upload."),
|
|
535
|
+
}, async ({ project, fileName, content }) => {
|
|
536
|
+
try {
|
|
537
|
+
if (fileName.includes(".."))
|
|
538
|
+
return { content: [{ type: "text", text: "Invalid fileName: path traversal is not allowed." }], isError: true };
|
|
539
|
+
if (!content)
|
|
540
|
+
return { content: [{ type: "text", text: "content is required for upload" }], isError: true };
|
|
541
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(content))
|
|
542
|
+
return { content: [{ type: "text", text: "content must be valid base64-encoded data" }], isError: true };
|
|
543
|
+
const connection = await connectionProvider();
|
|
544
|
+
let resolvedProject = project;
|
|
545
|
+
if (!resolvedProject) {
|
|
546
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to upload the work item attachment to.");
|
|
547
|
+
if ("response" in result)
|
|
548
|
+
return result.response;
|
|
549
|
+
resolvedProject = result.resolved;
|
|
550
|
+
}
|
|
551
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
552
|
+
const attachmentReference = await workItemApi.createAttachment({}, Readable.from(Buffer.from(content, "base64")), fileName, undefined, resolvedProject);
|
|
553
|
+
return { content: [{ type: "text", text: JSON.stringify(attachmentReference, null, 2) }] };
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
557
|
+
return {
|
|
558
|
+
content: [{ type: "text", text: `Error uploading work item attachment: ${errorMessage}` }],
|
|
559
|
+
isError: true,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
// --- wit_work_item_attachment_link ------------------------------------------
|
|
564
|
+
server.tool(WORKITEM_TOOLS.wit_work_item_attachment_link, "Link an uploaded attachment to a work item using the attachment URL returned by wit_work_item_attachment_upload.", {
|
|
565
|
+
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."),
|
|
566
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item to link the uploaded attachment to."),
|
|
567
|
+
attachmentUrl: z.string().url().describe("The attachment URL returned by wit_work_item_attachment_upload."),
|
|
568
|
+
comment: z.string().optional().describe("Optional comment to include with the attachment link."),
|
|
569
|
+
}, async ({ project, workItemId, attachmentUrl, comment }) => {
|
|
570
|
+
try {
|
|
571
|
+
const connection = await connectionProvider();
|
|
572
|
+
let resolvedProject = project;
|
|
573
|
+
if (!resolvedProject) {
|
|
574
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project containing the work item to link the attachment to.");
|
|
575
|
+
if ("response" in result)
|
|
576
|
+
return result.response;
|
|
577
|
+
resolvedProject = result.resolved;
|
|
578
|
+
}
|
|
579
|
+
const patchDocument = [
|
|
580
|
+
{
|
|
581
|
+
op: "add",
|
|
582
|
+
path: "/relations/-",
|
|
583
|
+
value: {
|
|
584
|
+
rel: "AttachedFile",
|
|
585
|
+
url: attachmentUrl,
|
|
586
|
+
attributes: { comment: comment || "" },
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
];
|
|
590
|
+
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
591
|
+
const workItem = await workItemApi.updateWorkItem({}, patchDocument, workItemId, resolvedProject);
|
|
592
|
+
if (!workItem)
|
|
593
|
+
throw new Error("Work item not found");
|
|
594
|
+
return { content: [{ type: "text", text: JSON.stringify({ attachmentUrl, workItemId }, null, 2) }] };
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
598
|
+
return {
|
|
599
|
+
content: [{ type: "text", text: `Error linking work item attachment: ${errorMessage}` }],
|
|
600
|
+
isError: true,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
});
|
|
485
604
|
// --- wit_work_item_write ----------------------------------------------------
|
|
486
605
|
server.tool(WORKITEM_TOOLS.wit_work_item_write, "Write operations for work items. Use the action parameter to specify the operation.", {
|
|
487
606
|
action: z
|
|
@@ -507,13 +626,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
507
626
|
.default("add")
|
|
508
627
|
.describe("The operation to perform. Use 'test' with path '/rev' to enforce optimistic concurrency."),
|
|
509
628
|
path: z.string().describe("The path to operate on, e.g. '/fields/System.Title' or '/rev' for a revision test."),
|
|
510
|
-
value:
|
|
511
|
-
.union([z.string(), z.number(), z.boolean(), z.null()])
|
|
629
|
+
value: jsonPatchValueSchema
|
|
512
630
|
.optional()
|
|
513
|
-
.describe("The operation value. Required for add, replace, and test; omit for remove. For a test on '/rev', pass the numeric revision previously read."),
|
|
631
|
+
.describe("The JSON operation value, including objects and arrays. Required for add, replace, and test; omit for remove. Pass relation objects directly, not as JSON strings. For a test on '/rev', pass the numeric revision previously read."),
|
|
514
632
|
}))
|
|
515
633
|
.optional()
|
|
516
|
-
.describe('
|
|
634
|
+
.describe('JSON Patch operations for fields and relations on a single work item. Required for: update. For a safe read-modify-write, prepend a test operation on "/rev" with value set to the numeric revision returned by the preceding read; Azure DevOps rejects the entire update if the current revision differs.'),
|
|
517
635
|
batchUpdates: z
|
|
518
636
|
.array(z.object({
|
|
519
637
|
op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform."),
|
|
@@ -746,7 +864,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
746
864
|
const orgUrl = connection.serverUrl;
|
|
747
865
|
const accessToken = await tokenProvider();
|
|
748
866
|
const formatParameter = (format ?? "Markdown") === "Markdown" ? 0 : 1;
|
|
749
|
-
const resolvedText = await
|
|
867
|
+
const resolvedText = await prepareCommentText(text, format, tokenProvider, connectionProvider, userAgentProvider);
|
|
750
868
|
if (action === "add") {
|
|
751
869
|
const response = await fetch(`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
|
|
752
870
|
method: "POST",
|
package/dist/tools.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { wrapExternalToolResponse } from "./shared/content-safety.js";
|
|
4
4
|
import { Domain } from "./shared/domains.js";
|
|
5
|
+
import { configureToolsWithAnnotations } from "./shared/tool-annotations.js";
|
|
5
6
|
import { configureAdvSecTools } from "./tools/advanced-security.js";
|
|
6
7
|
import { configureMcpAppsTools } from "./tools/mcp-apps.js";
|
|
7
8
|
import { configurePipelineTools } from "./tools/pipelines.js";
|
|
@@ -13,23 +14,25 @@ import { configureWikiTools } from "./tools/wiki.js";
|
|
|
13
14
|
import { configureWorkTools } from "./tools/work.js";
|
|
14
15
|
import { configureWorkItemTools } from "./tools/work-items.js";
|
|
15
16
|
function configureAllTools(server, tokenProvider, connectionProvider, userAgentProvider, enabledDomains) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
configureToolsWithAnnotations(server, () => {
|
|
18
|
+
const configureIfDomainEnabled = (domain, configureFn) => {
|
|
19
|
+
if (enabledDomains.has(domain)) {
|
|
20
|
+
configureToolsWithContentSafety(server, domain, configureFn);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
configureIfDomainEnabled(Domain.CORE, () => configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
24
|
+
// This is a local health-check response and contains no Azure DevOps content.
|
|
25
|
+
if (enabledDomains.has(Domain.MCP_APPS))
|
|
26
|
+
configureMcpAppsTools(server);
|
|
27
|
+
configureIfDomainEnabled(Domain.WORK, () => configureWorkTools(server, tokenProvider, connectionProvider));
|
|
28
|
+
configureIfDomainEnabled(Domain.PIPELINES, () => configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
29
|
+
configureIfDomainEnabled(Domain.REPOSITORIES, () => configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
30
|
+
configureIfDomainEnabled(Domain.WORK_ITEMS, () => configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
31
|
+
configureIfDomainEnabled(Domain.WIKI, () => configureWikiTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
32
|
+
configureIfDomainEnabled(Domain.TEST_PLANS, () => configureTestPlanTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
33
|
+
configureIfDomainEnabled(Domain.SEARCH, () => configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
34
|
+
configureIfDomainEnabled(Domain.ADVANCED_SECURITY, () => configureAdvSecTools(server, tokenProvider, connectionProvider));
|
|
35
|
+
});
|
|
33
36
|
}
|
|
34
37
|
/**
|
|
35
38
|
* Centralizes the untrusted-content boundary for tool responses. Tool registration
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.
|
|
1
|
+
export const packageVersion = "2.11.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-devops/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"mcpName": "microsoft.com/azure-devops",
|
|
5
5
|
"description": "MCP server for interacting with Azure DevOps",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,8 +40,7 @@
|
|
|
40
40
|
"@azure/identity": "^4.13.0",
|
|
41
41
|
"@azure/logger": "^1.3.0",
|
|
42
42
|
"@azure/msal-node": "^5.5.0",
|
|
43
|
-
"@
|
|
44
|
-
"@modelcontextprotocol/sdk": "1.29.0",
|
|
43
|
+
"@modelcontextprotocol/sdk": "1.31.0",
|
|
45
44
|
"azure-devops-extension-api": "^5.272.3",
|
|
46
45
|
"azure-devops-extension-sdk": "^4.0.2",
|
|
47
46
|
"azure-devops-node-api": "^15.1.2",
|
|
@@ -51,6 +50,9 @@
|
|
|
51
50
|
"zod": "^3.25.63",
|
|
52
51
|
"zod-to-json-schema": "^3.24.5"
|
|
53
52
|
},
|
|
53
|
+
"optionalDependencies": {
|
|
54
|
+
"@azure/msal-node-extensions": "^5.3.5"
|
|
55
|
+
},
|
|
54
56
|
"devDependencies": {
|
|
55
57
|
"@types/jest": "^30.0.0",
|
|
56
58
|
"@types/node": "^22.19.1",
|