@azure-devops/mcp 1.1.0 → 1.2.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 +167 -101
- package/dist/index.js +38 -9
- package/dist/tools/builds.js +14 -7
- package/dist/tools/core.js +48 -0
- package/dist/tools/releases.js +26 -7
- package/dist/tools/repos.js +41 -9
- package/dist/tools/search.js +85 -80
- package/dist/tools/workitems.js +108 -41
- package/dist/useragent.js +2 -0
- package/dist/utils.js +26 -0
- package/dist/version.js +1 -1
- package/package.json +6 -5
package/dist/tools/workitems.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
+
import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import { batchApiVersion } from "../utils.js";
|
|
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
|
+
}
|
|
5
14
|
const WORKITEM_TOOLS = {
|
|
6
15
|
my_work_items: "wit_my_work_items",
|
|
7
16
|
list_backlogs: "wit_list_backlogs",
|
|
@@ -42,6 +51,10 @@ function getLinkTypeFromName(name) {
|
|
|
42
51
|
return "Microsoft.VSTS.Common.TestedBy-Forward";
|
|
43
52
|
case "tests":
|
|
44
53
|
return "Microsoft.VSTS.Common.TestedBy-Reverse";
|
|
54
|
+
case "affects":
|
|
55
|
+
return "Microsoft.VSTS.Common.Affects-Forward";
|
|
56
|
+
case "affected by":
|
|
57
|
+
return "Microsoft.VSTS.Common.Affects-Reverse";
|
|
45
58
|
default:
|
|
46
59
|
throw new Error(`Unknown link type: ${name}`);
|
|
47
60
|
}
|
|
@@ -131,13 +144,30 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
131
144
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
132
145
|
workItemId: z.number().describe("The ID of the work item to add a comment to."),
|
|
133
146
|
comment: z.string().describe("The text of the comment to add to the work item."),
|
|
134
|
-
|
|
147
|
+
format: z.enum(["markdown", "html"]).optional().default("html"),
|
|
148
|
+
}, async ({ project, workItemId, comment, format }) => {
|
|
135
149
|
const connection = await connectionProvider();
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
150
|
+
const orgUrl = connection.serverUrl;
|
|
151
|
+
const accessToken = await tokenProvider();
|
|
152
|
+
const body = {
|
|
153
|
+
text: comment,
|
|
154
|
+
};
|
|
155
|
+
const formatParameter = format === "markdown" ? 0 : 1;
|
|
156
|
+
const response = await fetch(`${orgUrl}/${project}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: {
|
|
159
|
+
"Authorization": `Bearer ${accessToken.token}`,
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
"User-Agent": userAgentProvider(),
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify(body),
|
|
164
|
+
});
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
throw new Error(`Failed to add a work item comment: ${response.statusText}}`);
|
|
167
|
+
}
|
|
168
|
+
const comments = await response.text();
|
|
139
169
|
return {
|
|
140
|
-
content: [{ type: "text", text:
|
|
170
|
+
content: [{ type: "text", text: comments }],
|
|
141
171
|
};
|
|
142
172
|
});
|
|
143
173
|
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.", {
|
|
@@ -200,6 +230,13 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
200
230
|
value: item.areaPath,
|
|
201
231
|
});
|
|
202
232
|
}
|
|
233
|
+
if (item.iterationPath && item.iterationPath.trim().length > 0) {
|
|
234
|
+
ops.push({
|
|
235
|
+
op: "add",
|
|
236
|
+
path: "/fields/System.IterationPath",
|
|
237
|
+
value: item.iterationPath,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
203
240
|
if (item.format && item.format === "Markdown") {
|
|
204
241
|
ops.push({
|
|
205
242
|
op: "add",
|
|
@@ -212,13 +249,6 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
212
249
|
value: item.format,
|
|
213
250
|
});
|
|
214
251
|
}
|
|
215
|
-
if (item.iterationPath && item.iterationPath.trim().length > 0) {
|
|
216
|
-
ops.push({
|
|
217
|
-
op: "add",
|
|
218
|
-
path: "/fields/System.IterationPath",
|
|
219
|
-
value: item.iterationPath,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
252
|
return {
|
|
223
253
|
method: "PATCH",
|
|
224
254
|
uri: `/${project}/_apis/wit/workitems/$${workItemType}?api-version=${batchApiVersion}`,
|
|
@@ -254,17 +284,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
254
284
|
}
|
|
255
285
|
});
|
|
256
286
|
server.tool(WORKITEM_TOOLS.link_work_item_to_pull_request, "Link a single work item to an existing pull request.", {
|
|
257
|
-
|
|
287
|
+
projectId: z.string().describe("The project ID of the Azure DevOps project (note: project name is not valid)."),
|
|
258
288
|
repositoryId: z.string().describe("The ID of the repository containing the pull request. Do not use the repository name here, use the ID instead."),
|
|
259
289
|
pullRequestId: z.number().describe("The ID of the pull request to link to."),
|
|
260
290
|
workItemId: z.number().describe("The ID of the work item to link to the pull request."),
|
|
261
|
-
}, async ({
|
|
291
|
+
}, async ({ projectId, repositoryId, pullRequestId, workItemId }) => {
|
|
262
292
|
try {
|
|
263
293
|
const connection = await connectionProvider();
|
|
264
294
|
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
265
295
|
// Create artifact link relation using vstfs format
|
|
266
296
|
// Format: vstfs:///Git/PullRequestId/{project}/{repositoryId}/{pullRequestId}
|
|
267
|
-
const artifactPathValue = `${
|
|
297
|
+
const artifactPathValue = `${projectId}/${repositoryId}/${pullRequestId}`;
|
|
268
298
|
const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`;
|
|
269
299
|
// Use the PATCH document format for adding a relation
|
|
270
300
|
const patchDocument = [
|
|
@@ -281,7 +311,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
281
311
|
},
|
|
282
312
|
];
|
|
283
313
|
// Use the WorkItem API to update the work item with the new relation
|
|
284
|
-
const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId,
|
|
314
|
+
const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, projectId);
|
|
285
315
|
if (!workItem) {
|
|
286
316
|
return { content: [{ type: "text", text: "Work item update failed" }], isError: true };
|
|
287
317
|
}
|
|
@@ -323,15 +353,20 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
323
353
|
id: z.number().describe("The ID of the work item to update."),
|
|
324
354
|
updates: z
|
|
325
355
|
.array(z.object({
|
|
326
|
-
op: z.enum(["
|
|
356
|
+
op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform on the field."),
|
|
327
357
|
path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
|
|
328
|
-
value: z.string().describe("The new value for the field. This is required for '
|
|
358
|
+
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."),
|
|
329
359
|
}))
|
|
330
360
|
.describe("An array of field updates to apply to the work item."),
|
|
331
361
|
}, async ({ id, updates }) => {
|
|
332
362
|
const connection = await connectionProvider();
|
|
333
363
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
334
|
-
|
|
364
|
+
// Convert operation names to lowercase for API
|
|
365
|
+
const apiUpdates = updates.map((update) => ({
|
|
366
|
+
...update,
|
|
367
|
+
op: operationToApiString(update.op),
|
|
368
|
+
}));
|
|
369
|
+
const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id);
|
|
335
370
|
return {
|
|
336
371
|
content: [{ type: "text", text: JSON.stringify(updatedWorkItem, null, 2) }],
|
|
337
372
|
};
|
|
@@ -351,17 +386,33 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
351
386
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
352
387
|
workItemType: z.string().describe("The type of work item to create, e.g., 'Task', 'Bug', etc."),
|
|
353
388
|
fields: z
|
|
354
|
-
.
|
|
355
|
-
.describe("
|
|
389
|
+
.array(z.object({
|
|
390
|
+
name: z.string().describe("The name of the field, e.g., 'System.Title'."),
|
|
391
|
+
value: z.string().describe("The value of the field."),
|
|
392
|
+
format: z.enum(["Html", "Markdown"]).optional().describe("the format of the field value, e.g., 'Html', 'Markdown'. Optional, defaults to 'Html'."),
|
|
393
|
+
}))
|
|
394
|
+
.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."),
|
|
356
395
|
}, async ({ project, workItemType, fields }) => {
|
|
357
396
|
try {
|
|
358
397
|
const connection = await connectionProvider();
|
|
359
398
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
360
|
-
const document =
|
|
399
|
+
const document = fields.map(({ name, value }) => ({
|
|
361
400
|
op: "add",
|
|
362
|
-
path: `/fields/${
|
|
363
|
-
value,
|
|
401
|
+
path: `/fields/${name}`,
|
|
402
|
+
value: value,
|
|
364
403
|
}));
|
|
404
|
+
// Check if any field has format === "Markdown" and add the multilineFieldsFormat operation
|
|
405
|
+
// this should only happen for large text fields, but since we dont't know by field name, lets assume if the users
|
|
406
|
+
// passes a value longer than 50 characters, then we can set the format to Markdown
|
|
407
|
+
fields.forEach(({ name, value, format }) => {
|
|
408
|
+
if (value.length > 50 && format === "Markdown") {
|
|
409
|
+
document.push({
|
|
410
|
+
op: "add",
|
|
411
|
+
path: `/multilineFieldsFormat/${name}`,
|
|
412
|
+
value: "Markdown",
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
});
|
|
365
416
|
const newWorkItem = await workItemApi.createWorkItem(null, document, project, workItemType);
|
|
366
417
|
if (!newWorkItem) {
|
|
367
418
|
return { content: [{ type: "text", text: "Work item was not created" }], isError: true };
|
|
@@ -381,14 +432,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
381
432
|
server.tool(WORKITEM_TOOLS.get_query, "Get a query by its ID or path.", {
|
|
382
433
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
383
434
|
query: z.string().describe("The ID or path of the query to retrieve."),
|
|
384
|
-
expand: z
|
|
435
|
+
expand: z
|
|
436
|
+
.enum(getEnumKeys(QueryExpand))
|
|
437
|
+
.optional()
|
|
438
|
+
.describe("Optional expand parameter to include additional details in the response. Defaults to 'None'."),
|
|
385
439
|
depth: z.number().default(0).describe("Optional depth parameter to specify how deep to expand the query. Defaults to 0."),
|
|
386
440
|
includeDeleted: z.boolean().default(false).describe("Whether to include deleted items in the query results. Defaults to false."),
|
|
387
441
|
useIsoDateFormat: z.boolean().default(false).describe("Whether to use ISO date format in the response. Defaults to false."),
|
|
388
442
|
}, async ({ project, query, expand, depth, includeDeleted, useIsoDateFormat }) => {
|
|
389
443
|
const connection = await connectionProvider();
|
|
390
444
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
391
|
-
const queryDetails = await workItemApi.getQuery(project, query, expand, depth, includeDeleted, useIsoDateFormat);
|
|
445
|
+
const queryDetails = await workItemApi.getQuery(project, query, safeEnumConvert(QueryExpand, expand), depth, includeDeleted, useIsoDateFormat);
|
|
392
446
|
return {
|
|
393
447
|
content: [{ type: "text", text: JSON.stringify(queryDetails, null, 2) }],
|
|
394
448
|
};
|
|
@@ -411,10 +465,11 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
411
465
|
server.tool(WORKITEM_TOOLS.update_work_items_batch, "Update work items in batch", {
|
|
412
466
|
updates: z
|
|
413
467
|
.array(z.object({
|
|
414
|
-
op: z.enum(["
|
|
468
|
+
op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform on the field."),
|
|
415
469
|
id: z.number().describe("The ID of the work item to update."),
|
|
416
470
|
path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
|
|
417
471
|
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."),
|
|
472
|
+
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'."),
|
|
418
473
|
}))
|
|
419
474
|
.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)."),
|
|
420
475
|
}, async ({ updates }) => {
|
|
@@ -423,20 +478,32 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
423
478
|
const accessToken = await tokenProvider();
|
|
424
479
|
// Extract unique IDs from the updates array
|
|
425
480
|
const uniqueIds = Array.from(new Set(updates.map((update) => update.id)));
|
|
426
|
-
const body = uniqueIds.map((id) =>
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
headers: {
|
|
430
|
-
"Content-Type": "application/json-patch+json",
|
|
431
|
-
},
|
|
432
|
-
body: updates
|
|
433
|
-
.filter((update) => update.id === id)
|
|
434
|
-
.map(({ op, path, value }) => ({
|
|
481
|
+
const body = uniqueIds.map((id) => {
|
|
482
|
+
const workItemUpdates = updates.filter((update) => update.id === id);
|
|
483
|
+
const operations = workItemUpdates.map(({ op, path, value }) => ({
|
|
435
484
|
op: op,
|
|
436
485
|
path: path,
|
|
437
486
|
value: value,
|
|
438
|
-
}))
|
|
439
|
-
|
|
487
|
+
}));
|
|
488
|
+
// Add format operations for Markdown fields
|
|
489
|
+
workItemUpdates.forEach(({ path, value, format }) => {
|
|
490
|
+
if (format === "Markdown" && value && value.length > 50) {
|
|
491
|
+
operations.push({
|
|
492
|
+
op: "Add",
|
|
493
|
+
path: `/multilineFieldsFormat${path.replace("/fields", "")}`,
|
|
494
|
+
value: "Markdown",
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
return {
|
|
499
|
+
method: "PATCH",
|
|
500
|
+
uri: `/_apis/wit/workitems/${id}?api-version=${batchApiVersion}`,
|
|
501
|
+
headers: {
|
|
502
|
+
"Content-Type": "application/json-patch+json",
|
|
503
|
+
},
|
|
504
|
+
body: operations,
|
|
505
|
+
};
|
|
506
|
+
});
|
|
440
507
|
const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
|
|
441
508
|
method: "PATCH",
|
|
442
509
|
headers: {
|
|
@@ -461,9 +528,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
461
528
|
id: z.number().describe("The ID of the work item to update."),
|
|
462
529
|
linkToId: z.number().describe("The ID of the work item to link to."),
|
|
463
530
|
type: z
|
|
464
|
-
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests"])
|
|
531
|
+
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by"])
|
|
465
532
|
.default("related")
|
|
466
|
-
.describe("Type of link to create between the work items. Options include 'parent', 'child', 'duplicate', 'duplicate of', 'related', 'successor', 'predecessor', 'tested by', 'tests', '
|
|
533
|
+
.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'."),
|
|
467
534
|
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."),
|
|
468
535
|
}))
|
|
469
536
|
.describe(""),
|
package/dist/useragent.js
CHANGED
package/dist/utils.js
CHANGED
|
@@ -2,3 +2,29 @@
|
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
export const apiVersion = "7.2-preview.1";
|
|
4
4
|
export const batchApiVersion = "5.0";
|
|
5
|
+
export const markdownCommentsApiVersion = "7.2-preview.4";
|
|
6
|
+
/**
|
|
7
|
+
* Converts a TypeScript numeric enum to an array of string keys for use with z.enum().
|
|
8
|
+
* This ensures that enum schemas generate string values rather than numeric values.
|
|
9
|
+
* @param enumObject The TypeScript enum object
|
|
10
|
+
* @returns Array of string keys from the enum
|
|
11
|
+
*/
|
|
12
|
+
export function getEnumKeys(enumObject) {
|
|
13
|
+
return Object.keys(enumObject).filter((key) => isNaN(Number(key)));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Safely converts a string enum key to its corresponding enum value.
|
|
17
|
+
* Validates that the key exists in the enum before conversion.
|
|
18
|
+
* @param enumObject The TypeScript enum object
|
|
19
|
+
* @param key The string key to convert
|
|
20
|
+
* @returns The enum value if key is valid, undefined otherwise
|
|
21
|
+
*/
|
|
22
|
+
export function safeEnumConvert(enumObject, key) {
|
|
23
|
+
if (!key)
|
|
24
|
+
return undefined;
|
|
25
|
+
const validKeys = getEnumKeys(enumObject);
|
|
26
|
+
if (!validKeys.includes(key)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return enumObject[key];
|
|
30
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "1.
|
|
1
|
+
export const packageVersion = "1.2.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-devops/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "MCP server for interacting with Azure DevOps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Microsoft Corporation",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
27
27
|
"prepare": "npm run build",
|
|
28
28
|
"watch": "tsc --watch",
|
|
29
|
-
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
29
|
+
"inspect": "ALLOWED_ORIGINS=http://127.0.0.1:6274 npx @modelcontextprotocol/inspector node dist/index.js",
|
|
30
30
|
"start": "node -r tsconfig-paths/register dist/index.js",
|
|
31
31
|
"eslint": "eslint",
|
|
32
32
|
"eslint-fix": "eslint --fix",
|
|
@@ -37,18 +37,19 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@azure/identity": "^4.10.0",
|
|
40
|
-
"@modelcontextprotocol/sdk": "1.
|
|
40
|
+
"@modelcontextprotocol/sdk": "1.16.0",
|
|
41
41
|
"azure-devops-extension-api": "^4.252.0",
|
|
42
42
|
"azure-devops-extension-sdk": "^4.0.2",
|
|
43
43
|
"azure-devops-node-api": "^15.1.0",
|
|
44
|
+
"yargs": "^18.0.0",
|
|
44
45
|
"zod": "^3.25.63",
|
|
45
46
|
"zod-to-json-schema": "^3.24.5"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
|
-
"@modelcontextprotocol/inspector": "^0.
|
|
49
|
+
"@modelcontextprotocol/inspector": "^0.16.1",
|
|
49
50
|
"@types/jest": "^30.0.0",
|
|
50
51
|
"@types/node": "^22",
|
|
51
|
-
"eslint-config-prettier": "10.1.
|
|
52
|
+
"eslint-config-prettier": "10.1.8",
|
|
52
53
|
"eslint-plugin-header": "^3.1.1",
|
|
53
54
|
"jest": "^30.0.2",
|
|
54
55
|
"jest-extended": "^6.0.0",
|