@azure-devops/mcp 1.2.0-daily.20250715 → 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.
@@ -1,7 +1,8 @@
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";
5
6
  const WORKITEM_TOOLS = {
6
7
  my_work_items: "wit_my_work_items",
7
8
  list_backlogs: "wit_list_backlogs",
@@ -19,7 +20,6 @@ const WORKITEM_TOOLS = {
19
20
  get_query: "wit_get_query",
20
21
  get_query_results_by_id: "wit_get_query_results_by_id",
21
22
  update_work_items_batch: "wit_update_work_items_batch",
22
- close_and_link_workitem_duplicates: "wit_close_and_link_workitem_duplicates",
23
23
  work_items_link: "wit_work_items_link",
24
24
  };
25
25
  function getLinkTypeFromName(name) {
@@ -42,6 +42,10 @@ function getLinkTypeFromName(name) {
42
42
  return "Microsoft.VSTS.Common.TestedBy-Forward";
43
43
  case "tests":
44
44
  return "Microsoft.VSTS.Common.TestedBy-Reverse";
45
+ case "affects":
46
+ return "Microsoft.VSTS.Common.Affects-Forward";
47
+ case "affected by":
48
+ return "Microsoft.VSTS.Common.Affects-Reverse";
45
49
  default:
46
50
  throw new Error(`Unknown link type: ${name}`);
47
51
  }
@@ -131,13 +135,30 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
131
135
  project: z.string().describe("The name or ID of the Azure DevOps project."),
132
136
  workItemId: z.number().describe("The ID of the work item to add a comment to."),
133
137
  comment: z.string().describe("The text of the comment to add to the work item."),
134
- }, async ({ project, workItemId, comment }) => {
138
+ format: z.enum(["markdown", "html"]).optional().default("html"),
139
+ }, async ({ project, workItemId, comment, format }) => {
135
140
  const connection = await connectionProvider();
136
- const workItemApi = await connection.getWorkItemTrackingApi();
137
- const commentCreate = { text: comment };
138
- const commentResponse = await workItemApi.addComment(commentCreate, project, workItemId);
141
+ const orgUrl = connection.serverUrl;
142
+ const accessToken = await tokenProvider();
143
+ const body = {
144
+ text: comment,
145
+ };
146
+ const formatParameter = format === "markdown" ? 0 : 1;
147
+ const response = await fetch(`${orgUrl}/${project}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
148
+ method: "POST",
149
+ headers: {
150
+ "Authorization": `Bearer ${accessToken.token}`,
151
+ "Content-Type": "application/json",
152
+ "User-Agent": userAgentProvider(),
153
+ },
154
+ body: JSON.stringify(body),
155
+ });
156
+ if (!response.ok) {
157
+ throw new Error(`Failed to add a work item comment: ${response.statusText}}`);
158
+ }
159
+ const comments = await response.text();
139
160
  return {
140
- content: [{ type: "text", text: JSON.stringify(commentResponse, null, 2) }],
161
+ content: [{ type: "text", text: comments }],
141
162
  };
142
163
  });
143
164
  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 +221,13 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
200
221
  value: item.areaPath,
201
222
  });
202
223
  }
224
+ if (item.iterationPath && item.iterationPath.trim().length > 0) {
225
+ ops.push({
226
+ op: "add",
227
+ path: "/fields/System.IterationPath",
228
+ value: item.iterationPath,
229
+ });
230
+ }
203
231
  if (item.format && item.format === "Markdown") {
204
232
  ops.push({
205
233
  op: "add",
@@ -212,13 +240,6 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
212
240
  value: item.format,
213
241
  });
214
242
  }
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
243
  return {
223
244
  method: "PATCH",
224
245
  uri: `/${project}/_apis/wit/workitems/$${workItemType}?api-version=${batchApiVersion}`,
@@ -254,17 +275,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
254
275
  }
255
276
  });
256
277
  server.tool(WORKITEM_TOOLS.link_work_item_to_pull_request, "Link a single work item to an existing pull request.", {
257
- project: z.string().describe("The name or ID of the Azure DevOps project."),
278
+ projectId: z.string().describe("The project ID of the Azure DevOps project (note: project name is not valid)."),
258
279
  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
280
  pullRequestId: z.number().describe("The ID of the pull request to link to."),
260
281
  workItemId: z.number().describe("The ID of the work item to link to the pull request."),
261
- }, async ({ project, repositoryId, pullRequestId, workItemId }) => {
282
+ }, async ({ projectId, repositoryId, pullRequestId, workItemId }) => {
262
283
  try {
263
284
  const connection = await connectionProvider();
264
285
  const workItemTrackingApi = await connection.getWorkItemTrackingApi();
265
286
  // Create artifact link relation using vstfs format
266
287
  // Format: vstfs:///Git/PullRequestId/{project}/{repositoryId}/{pullRequestId}
267
- const artifactPathValue = `${project}/${repositoryId}/${pullRequestId}`;
288
+ const artifactPathValue = `${projectId}/${repositoryId}/${pullRequestId}`;
268
289
  const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`;
269
290
  // Use the PATCH document format for adding a relation
270
291
  const patchDocument = [
@@ -281,7 +302,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
281
302
  },
282
303
  ];
283
304
  // Use the WorkItem API to update the work item with the new relation
284
- const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, project);
305
+ const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, projectId);
285
306
  if (!workItem) {
286
307
  return { content: [{ type: "text", text: "Work item update failed" }], isError: true };
287
308
  }
@@ -323,15 +344,25 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
323
344
  id: z.number().describe("The ID of the work item to update."),
324
345
  updates: z
325
346
  .array(z.object({
326
- op: z.enum(["add", "replace", "remove"]).default("add").describe("The operation to perform on the field."),
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."),
327
353
  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 'add' and 'replace' operations, and should be omitted for 'remove' operations."),
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."),
329
355
  }))
330
356
  .describe("An array of field updates to apply to the work item."),
331
357
  }, async ({ id, updates }) => {
332
358
  const connection = await connectionProvider();
333
359
  const workItemApi = await connection.getWorkItemTrackingApi();
334
- const updatedWorkItem = await workItemApi.updateWorkItem(null, updates, id);
360
+ // Convert operation names to lowercase for API
361
+ const apiUpdates = updates.map((update) => ({
362
+ ...update,
363
+ op: update.op,
364
+ }));
365
+ const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id);
335
366
  return {
336
367
  content: [{ type: "text", text: JSON.stringify(updatedWorkItem, null, 2) }],
337
368
  };
@@ -351,17 +382,33 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
351
382
  project: z.string().describe("The name or ID of the Azure DevOps project."),
352
383
  workItemType: z.string().describe("The type of work item to create, e.g., 'Task', 'Bug', etc."),
353
384
  fields: z
354
- .record(z.string(), z.string())
355
- .describe("A record of field names and values to set on the new work item. Each key is a field name, and each value is the corresponding value to set for that field."),
385
+ .array(z.object({
386
+ name: z.string().describe("The name of the field, e.g., 'System.Title'."),
387
+ value: z.string().describe("The value of the field."),
388
+ format: z.enum(["Html", "Markdown"]).optional().describe("the format of the field value, e.g., 'Html', 'Markdown'. Optional, defaults to 'Html'."),
389
+ }))
390
+ .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
391
  }, async ({ project, workItemType, fields }) => {
357
392
  try {
358
393
  const connection = await connectionProvider();
359
394
  const workItemApi = await connection.getWorkItemTrackingApi();
360
- const document = Object.entries(fields).map(([key, value]) => ({
395
+ const document = fields.map(({ name, value }) => ({
361
396
  op: "add",
362
- path: `/fields/${key}`,
363
- value,
397
+ path: `/fields/${name}`,
398
+ value: value,
364
399
  }));
400
+ // Check if any field has format === "Markdown" and add the multilineFieldsFormat operation
401
+ // this should only happen for large text fields, but since we dont't know by field name, lets assume if the users
402
+ // passes a value longer than 50 characters, then we can set the format to Markdown
403
+ fields.forEach(({ name, value, format }) => {
404
+ if (value.length > 50 && format === "Markdown") {
405
+ document.push({
406
+ op: "add",
407
+ path: `/multilineFieldsFormat/${name}`,
408
+ value: "Markdown",
409
+ });
410
+ }
411
+ });
365
412
  const newWorkItem = await workItemApi.createWorkItem(null, document, project, workItemType);
366
413
  if (!newWorkItem) {
367
414
  return { content: [{ type: "text", text: "Work item was not created" }], isError: true };
@@ -381,14 +428,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
381
428
  server.tool(WORKITEM_TOOLS.get_query, "Get a query by its ID or path.", {
382
429
  project: z.string().describe("The name or ID of the Azure DevOps project."),
383
430
  query: z.string().describe("The ID or path of the query to retrieve."),
384
- expand: z.enum(["all", "clauses", "minimal", "none", "wiql"]).optional().describe("Optional expand parameter to include additional details in the response. Defaults to 'none'."),
431
+ expand: z
432
+ .enum(getEnumKeys(QueryExpand))
433
+ .optional()
434
+ .describe("Optional expand parameter to include additional details in the response. Defaults to 'None'."),
385
435
  depth: z.number().default(0).describe("Optional depth parameter to specify how deep to expand the query. Defaults to 0."),
386
436
  includeDeleted: z.boolean().default(false).describe("Whether to include deleted items in the query results. Defaults to false."),
387
437
  useIsoDateFormat: z.boolean().default(false).describe("Whether to use ISO date format in the response. Defaults to false."),
388
438
  }, async ({ project, query, expand, depth, includeDeleted, useIsoDateFormat }) => {
389
439
  const connection = await connectionProvider();
390
440
  const workItemApi = await connection.getWorkItemTrackingApi();
391
- const queryDetails = await workItemApi.getQuery(project, query, expand, depth, includeDeleted, useIsoDateFormat);
441
+ const queryDetails = await workItemApi.getQuery(project, query, safeEnumConvert(QueryExpand, expand), depth, includeDeleted, useIsoDateFormat);
392
442
  return {
393
443
  content: [{ type: "text", text: JSON.stringify(queryDetails, null, 2) }],
394
444
  };
@@ -411,10 +461,11 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
411
461
  server.tool(WORKITEM_TOOLS.update_work_items_batch, "Update work items in batch", {
412
462
  updates: z
413
463
  .array(z.object({
414
- op: z.enum(["add", "replace", "remove"]).default("add").describe("The operation to perform on the field."),
464
+ op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform on the field."),
415
465
  id: z.number().describe("The ID of the work item to update."),
416
466
  path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
417
467
  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."),
468
+ 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
469
  }))
419
470
  .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
471
  }, async ({ updates }) => {
@@ -423,20 +474,32 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
423
474
  const accessToken = await tokenProvider();
424
475
  // Extract unique IDs from the updates array
425
476
  const uniqueIds = Array.from(new Set(updates.map((update) => update.id)));
426
- const body = uniqueIds.map((id) => ({
427
- method: "PATCH",
428
- uri: `/_apis/wit/workitems/${id}?api-version=${batchApiVersion}`,
429
- headers: {
430
- "Content-Type": "application/json-patch+json",
431
- },
432
- body: updates
433
- .filter((update) => update.id === id)
434
- .map(({ op, path, value }) => ({
477
+ const body = uniqueIds.map((id) => {
478
+ const workItemUpdates = updates.filter((update) => update.id === id);
479
+ const operations = workItemUpdates.map(({ op, path, value }) => ({
435
480
  op: op,
436
481
  path: path,
437
482
  value: value,
438
- })),
439
- }));
483
+ }));
484
+ // Add format operations for Markdown fields
485
+ workItemUpdates.forEach(({ path, value, format }) => {
486
+ if (format === "Markdown" && value && value.length > 50) {
487
+ operations.push({
488
+ op: "Add",
489
+ path: `/multilineFieldsFormat${path.replace("/fields", "")}`,
490
+ value: "Markdown",
491
+ });
492
+ }
493
+ });
494
+ return {
495
+ method: "PATCH",
496
+ uri: `/_apis/wit/workitems/${id}?api-version=${batchApiVersion}`,
497
+ headers: {
498
+ "Content-Type": "application/json-patch+json",
499
+ },
500
+ body: operations,
501
+ };
502
+ });
440
503
  const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
441
504
  method: "PATCH",
442
505
  headers: {
@@ -461,9 +524,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
461
524
  id: z.number().describe("The ID of the work item to update."),
462
525
  linkToId: z.number().describe("The ID of the work item to link to."),
463
526
  type: z
464
- .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests"])
527
+ .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by"])
465
528
  .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', 'referenced by', and 'references'. Defaults to 'related'."),
529
+ .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
530
  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
531
  }))
469
532
  .describe(""),
@@ -510,52 +573,5 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
510
573
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
511
574
  };
512
575
  });
513
- server.tool(WORKITEM_TOOLS.close_and_link_workitem_duplicates, "Close duplicate work items by id.", {
514
- id: z.number().describe("The ID of the work item to close and link duplicates to."),
515
- duplicateIds: z.array(z.number()).describe("An array of IDs of the duplicate work items to close and link to the specified work item."),
516
- project: z.string().describe("The name or ID of the Azure DevOps project."),
517
- state: z.string().default("Removed").describe("The state to set for the duplicate work items. Defaults to 'Removed'."),
518
- }, async ({ id, duplicateIds, project, state }) => {
519
- const connection = await connectionProvider();
520
- const body = duplicateIds.map((duplicateId) => ({
521
- method: "PATCH",
522
- uri: `/_apis/wit/workitems/${duplicateId}?api-version=${batchApiVersion}`,
523
- headers: {
524
- "Content-Type": "application/json-patch+json",
525
- },
526
- body: [
527
- {
528
- op: "add",
529
- path: "/fields/System.State",
530
- value: `${state}`,
531
- },
532
- {
533
- op: "add",
534
- path: "/relations/-",
535
- value: {
536
- rel: "System.LinkTypes.Duplicate-Reverse",
537
- url: `${connection.serverUrl}/${project}/_apis/wit/workItems/${id}`,
538
- },
539
- },
540
- ],
541
- }));
542
- const accessToken = await tokenProvider();
543
- const response = await fetch(`${connection.serverUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
544
- method: "PATCH",
545
- headers: {
546
- "Authorization": `Bearer ${accessToken.token}`,
547
- "Content-Type": "application/json",
548
- "User-Agent": userAgentProvider(),
549
- },
550
- body: JSON.stringify(body),
551
- });
552
- if (!response.ok) {
553
- throw new Error(`Failed to update work items in batch: ${response.statusText}`);
554
- }
555
- const result = await response.json();
556
- return {
557
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
558
- };
559
- });
560
576
  }
561
577
  export { WORKITEM_TOOLS, configureWorkItemTools };
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.2.0-daily.20250715";
1
+ export const packageVersion = "1.2.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "1.2.0-daily.20250715",
3
+ "version": "1.2.1",
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.13.2",
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.15.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.5",
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",