@azure-devops/mcp 2.9.0 → 2.10.0-nightly.20260910

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.
@@ -8,6 +8,7 @@ import { z } from "zod";
8
8
  import { batchApiVersion, markdownCommentsApiVersion, getEnumKeys, safeEnumConvert, encodeFormattedValue } from "../utils.js";
9
9
  import { elicitProject, elicitTeam } from "../shared/elicitations.js";
10
10
  import { createExternalContentResponse } from "../shared/content-safety.js";
11
+ import { getUserIdentityFromEmail } from "./auth.js";
11
12
  const WORKITEM_TOOLS = {
12
13
  wit_work_item: "wit_work_item",
13
14
  wit_query: "wit_query",
@@ -43,6 +44,8 @@ function getLinkTypeFromName(name) {
43
44
  return "Microsoft.VSTS.Common.Affects-Reverse";
44
45
  case "artifact":
45
46
  return "ArtifactLink";
47
+ case "hyperlink":
48
+ return "Hyperlink";
46
49
  default:
47
50
  throw new Error(`Unknown link type: ${name}`);
48
51
  }
@@ -55,13 +58,46 @@ function getArtifactLinkAttributeName(linkType) {
55
58
  return linkType;
56
59
  }
57
60
  }
61
+ function escapeHtml(value) {
62
+ const entities = {
63
+ "&": "&",
64
+ "<": "&lt;",
65
+ ">": "&gt;",
66
+ '"': "&quot;",
67
+ "'": "&#39;",
68
+ };
69
+ return value.replace(/[&<>"']/g, (character) => entities[character]);
70
+ }
71
+ async function resolveCommentMentions(text, format, tokenProvider, connectionProvider, userAgentProvider) {
72
+ const emailMatches = [...text.matchAll(/@<([^<>\s]+@[^<>\s]+)>/g)];
73
+ if (emailMatches.length === 0)
74
+ return text;
75
+ const identities = new Map();
76
+ for (const email of new Set(emailMatches.map((match) => match[1]))) {
77
+ try {
78
+ identities.set(email, await getUserIdentityFromEmail(email, tokenProvider, connectionProvider, userAgentProvider));
79
+ }
80
+ catch {
81
+ // Leave mentions unchanged when their identities cannot be resolved.
82
+ }
83
+ }
84
+ return text.replace(/@<([^<>\s]+@[^<>\s]+)>/g, (mention, email) => {
85
+ const identity = identities.get(email);
86
+ if (!identity)
87
+ return escapeHtml(mention);
88
+ return format === "Markdown" || format === undefined ? `@<${identity.id}>` : `<a href="#" data-vss-mention="version:2.0,${identity.id}">@${escapeHtml(identity.displayName)}</a>`;
89
+ });
90
+ }
58
91
  function configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider) {
59
92
  // --- wit_work_item ----------------------------------------------------------
60
- server.tool(WORKITEM_TOOLS.wit_work_item, "Retrieve work item data for a project. Use the action parameter to specify the operation.", {
93
+ server.tool(WORKITEM_TOOLS.wit_work_item, "Retrieve work item data. Use the action parameter to specify the operation.", {
61
94
  action: z
62
95
  .enum(["get", "get_batch", "list_comments", "my", "list_revisions", "list_for_iteration", "get_type"])
63
96
  .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)."),
64
- 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."),
97
+ project: z
98
+ .string()
99
+ .optional()
100
+ .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."),
65
101
  id: z.coerce.number().min(1).optional().describe("Work item ID. Required for: get."),
66
102
  ids: z.array(z.coerce.number().min(1)).optional().describe("Work item IDs. Required for: get_batch."),
67
103
  workItemId: z.coerce.number().min(1).optional().describe("Work item ID. Required for: list_comments, list_revisions."),
@@ -85,12 +121,6 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
85
121
  if (action === "get") {
86
122
  if (!id)
87
123
  return { content: [{ type: "text", text: "id is required for get" }], isError: true };
88
- if (!resolvedProject) {
89
- const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the work item from.");
90
- if ("response" in result)
91
- return result.response;
92
- resolvedProject = result.resolved;
93
- }
94
124
  let effectiveExpand = expand;
95
125
  if (fields && fields.length > 0 && effectiveExpand != null) {
96
126
  effectiveExpand = "none";
@@ -309,11 +339,24 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
309
339
  });
310
340
  // --- wit_backlog ------------------------------------------------------------
311
341
  server.tool(WORKITEM_TOOLS.wit_backlog, "Retrieve backlog data for a project and team. Use the action parameter to specify the operation.", {
312
- action: z.enum(["list", "list_work_items"]).describe("The action to perform. Options: list (list backlog levels for a team), list_work_items (list work items in a specific backlog level)."),
342
+ action: z
343
+ .enum(["list", "list_work_items", "reorder"])
344
+ .describe("The action to perform. Options: list (list backlog levels for a team), list_work_items (list work items in a specific backlog level), reorder (move work items to a new position in a backlog or iteration)."),
313
345
  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."),
314
346
  team: z.string().optional().describe("The name or ID of the Azure DevOps team. Reuse from prior context if already known. If not provided, a team selection prompt will be shown."),
315
347
  backlogId: z.string().optional().describe("The ID of the backlog category to retrieve work items from. Required for: list_work_items."),
316
- }, async ({ action, project, team, backlogId }) => {
348
+ ids: z.array(z.coerce.number().int().min(1)).min(1).optional().describe("The IDs of the work items to reorder. Required for: reorder."),
349
+ previousId: z.coerce
350
+ .number()
351
+ .int()
352
+ .min(0)
353
+ .optional()
354
+ .describe("The ID of the work item that should be before the reordered items. Use 0 to specify the beginning of the list. Optional for: reorder."),
355
+ nextId: z.coerce.number().int().min(0).optional().describe("The ID of the work item that should be after the reordered items. Use 0 to specify the end of the list. Optional for: reorder."),
356
+ parentId: z.coerce.number().int().min(0).optional().describe("The parent ID for all work items involved in the operation. Use 0 to indicate the items have no parent. Optional for: reorder."),
357
+ iterationPath: z.string().optional().describe("The iteration path for the reorder operation. Used when reordering items in an iteration backlog. Optional for: reorder."),
358
+ iterationId: z.string().optional().describe("The iteration ID. When provided, reorder items in that iteration instead of the team backlog. Used for: reorder."),
359
+ }, async ({ action, project, team, backlogId, ids, previousId, nextId, parentId, iterationPath, iterationId }) => {
317
360
  try {
318
361
  const connection = await connectionProvider();
319
362
  let resolvedProject = project;
@@ -344,6 +387,13 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
344
387
  const workItems = await workApi.getBacklogLevelWorkItems(teamContext, backlogId);
345
388
  return { content: [{ type: "text", text: JSON.stringify(workItems, null, 2) }] };
346
389
  }
390
+ if (action === "reorder") {
391
+ if (!ids?.length)
392
+ return { content: [{ type: "text", text: "ids is required for reorder" }], isError: true };
393
+ const operation = { ids, previousId, nextId, parentId, iterationPath };
394
+ const reorderedItems = iterationId ? await workApi.reorderIterationWorkItems(operation, teamContext, iterationId) : await workApi.reorderBacklogWorkItems(operation, teamContext);
395
+ return { content: [{ type: "text", text: JSON.stringify(reorderedItems, null, 2) }] };
396
+ }
347
397
  return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
348
398
  }
349
399
  catch (error) {
@@ -351,6 +401,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
351
401
  const msgs = {
352
402
  list: `Error listing backlogs: ${errorMessage}`,
353
403
  list_work_items: `Error listing backlog work items: ${errorMessage}`,
404
+ reorder: `Error reordering backlog work items: ${errorMessage}`,
354
405
  };
355
406
  return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
356
407
  }
@@ -404,9 +455,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
404
455
  }
405
456
  const mimeType = getMimeType(fileName);
406
457
  if (mimeType.startsWith("text/")) {
407
- return {
408
- content: [{ type: "text", text: buffer.toString("utf-8") }],
409
- };
458
+ return createExternalContentResponse(buffer.toString("utf-8"), "work item attachment");
410
459
  }
411
460
  const base64Data = buffer.toString("base64");
412
461
  return {
@@ -451,14 +500,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
451
500
  op: z
452
501
  .string()
453
502
  .transform((val) => val.toLowerCase())
454
- .pipe(z.enum(["add", "replace", "remove"]))
503
+ .pipe(z.enum(["add", "replace", "remove", "test"]))
455
504
  .default("add")
456
- .describe("The operation to perform."),
457
- path: z.string().describe("The field path, e.g. '/fields/System.Title'."),
458
- value: z.string().describe("The new value for the field."),
505
+ .describe("The operation to perform. Use 'test' with path '/rev' to enforce optimistic concurrency."),
506
+ path: z.string().describe("The path to operate on, e.g. '/fields/System.Title' or '/rev' for a revision test."),
507
+ value: z
508
+ .union([z.string(), z.number(), z.boolean(), z.null()])
509
+ .optional()
510
+ .describe("The operation value. Required for add, replace, and test; omit for remove. For a test on '/rev', pass the numeric revision previously read."),
459
511
  }))
460
512
  .optional()
461
- .describe("Field updates for a single work item. Required for: update."),
513
+ .describe('Field updates for 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.'),
462
514
  batchUpdates: z
463
515
  .array(z.object({
464
516
  op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform."),
@@ -521,6 +573,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
521
573
  return { content: [{ type: "text", text: "id is required for update" }], isError: true };
522
574
  if (!updates || updates.length === 0)
523
575
  return { content: [{ type: "text", text: "updates is required for update" }], isError: true };
576
+ const updateWithoutValue = updates.find((update) => update.op !== "remove" && update.value === undefined);
577
+ if (updateWithoutValue) {
578
+ return { content: [{ type: "text", text: `value is required for ${updateWithoutValue.op}` }], isError: true };
579
+ }
524
580
  const workItemApi = await connection.getWorkItemTrackingApi();
525
581
  const apiUpdates = updates.map((update) => ({ ...update, op: update.op }));
526
582
  const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id);
@@ -593,8 +649,6 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
593
649
  const ops = [
594
650
  { op: "add", path: "/id", value: `-${x + 1}` },
595
651
  { op: "add", path: "/fields/System.Title", value: item.title },
596
- { op: "add", path: "/fields/System.Description", value: encodedDescription },
597
- { op: "add", path: "/fields/Microsoft.VSTS.TCM.ReproSteps", value: encodedDescription },
598
652
  {
599
653
  op: "add",
600
654
  path: "/relations/-",
@@ -610,9 +664,19 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
610
664
  if (item.iterationPath && item.iterationPath.trim().length > 0) {
611
665
  ops.push({ op: "add", path: "/fields/System.IterationPath", value: item.iterationPath });
612
666
  }
613
- if (item.format && item.format === "Markdown") {
614
- ops.push({ op: "add", path: "/multilineFieldsFormat/System.Description", value: item.format });
615
- ops.push({ op: "add", path: "/multilineFieldsFormat/Microsoft.VSTS.TCM.ReproSteps", value: item.format });
667
+ // check if the work item type is "Bug" to determine which field to use for the description
668
+ // ReproSteps is used for Bugs, while Description is used for other work item types
669
+ if (workItemType.toLowerCase() === "bug") {
670
+ ops.push({ op: "add", path: "/fields/Microsoft.VSTS.TCM.ReproSteps", value: encodedDescription });
671
+ if (item.format && item.format === "Markdown") {
672
+ ops.push({ op: "add", path: "/multilineFieldsFormat/Microsoft.VSTS.TCM.ReproSteps", value: item.format });
673
+ }
674
+ }
675
+ else {
676
+ ops.push({ op: "add", path: "/fields/System.Description", value: encodedDescription });
677
+ if (item.format && item.format === "Markdown") {
678
+ ops.push({ op: "add", path: "/multilineFieldsFormat/System.Description", value: item.format });
679
+ }
616
680
  }
617
681
  return {
618
682
  method: "PATCH",
@@ -641,9 +705,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
641
705
  }
642
706
  catch (error) {
643
707
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
708
+ const statusCode = typeof error === "object" && error !== null && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : undefined;
709
+ const statusText = statusCode === 409 ? " Conflict" : statusCode === 412 ? " Precondition Failed" : "";
710
+ const updateStatus = statusCode !== undefined ? ` [HTTP ${statusCode}${statusText}]` : "";
644
711
  const msgs = {
645
712
  create: `Error creating work item: ${errorMessage}`,
646
- update: `Error updating work item: ${errorMessage}`,
713
+ update: `Error updating work item${updateStatus}: ${errorMessage}`,
647
714
  update_batch: `Error updating work items in batch: ${errorMessage}`,
648
715
  add_child: `Error creating child work items: ${errorMessage}`,
649
716
  };
@@ -676,6 +743,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
676
743
  const orgUrl = connection.serverUrl;
677
744
  const accessToken = await tokenProvider();
678
745
  const formatParameter = (format ?? "Markdown") === "Markdown" ? 0 : 1;
746
+ const resolvedText = await resolveCommentMentions(text, format, tokenProvider, connectionProvider, userAgentProvider);
679
747
  if (action === "add") {
680
748
  const response = await fetch(`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
681
749
  method: "POST",
@@ -684,7 +752,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
684
752
  "Content-Type": "application/json",
685
753
  "User-Agent": userAgentProvider(),
686
754
  },
687
- body: JSON.stringify({ text }),
755
+ body: JSON.stringify({ text: resolvedText }),
688
756
  });
689
757
  if (!response.ok) {
690
758
  throw new Error(`Failed to add a work item comment: ${response.statusText}`);
@@ -701,7 +769,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
701
769
  "Content-Type": "application/json",
702
770
  "User-Agent": userAgentProvider(),
703
771
  },
704
- body: JSON.stringify({ text }),
772
+ body: JSON.stringify({ text: resolvedText }),
705
773
  });
706
774
  if (!response.ok) {
707
775
  throw new Error(`Failed to update work item comment: ${response.statusText}`);
@@ -729,9 +797,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
729
797
  updates: z
730
798
  .array(z.object({
731
799
  id: z.coerce.number().min(1).describe("The ID of the work item to update."),
732
- linkToId: z.coerce.number().min(1).describe("The ID of the work item to link to."),
800
+ linkToId: z.coerce.number().min(1).optional().describe("The ID of the work item to link to. Required unless type is 'hyperlink'."),
801
+ url: z.string().optional().describe("The URL for a hyperlink. Required when type is 'hyperlink'."),
733
802
  type: z
734
- .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by"])
803
+ .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "hyperlink"])
735
804
  .default("related")
736
805
  .describe("Type of link. Defaults to 'related'."),
737
806
  comment: z.string().optional().describe("Optional comment for the link."),
@@ -741,7 +810,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
741
810
  // unlink
742
811
  id: z.coerce.number().min(1).optional().describe("Work item ID to remove links from. Required for: unlink."),
743
812
  type: z
744
- .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "artifact"])
813
+ .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "artifact", "hyperlink"])
745
814
  .optional()
746
815
  .describe("Link type to remove. Required for: unlink."),
747
816
  url: z.string().optional().describe("URL to match when removing a link. Used for: unlink. If not provided, all links of the specified type are removed."),
@@ -801,15 +870,23 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
801
870
  headers: { "Content-Type": "application/json-patch+json" },
802
871
  body: updates
803
872
  .filter((update) => update.id === uid)
804
- .map(({ linkToId, type: linkTypeName, comment: linkComment }) => ({
805
- op: "add",
806
- path: "/relations/-",
807
- value: {
808
- rel: `${getLinkTypeFromName(linkTypeName)}`,
809
- url: `${orgUrl}/${resolvedProject}/_apis/wit/workItems/${linkToId}`,
810
- attributes: { comment: linkComment || "" },
811
- },
812
- })),
873
+ .map(({ linkToId, url: linkUrl, type: linkTypeName, comment: linkComment }) => {
874
+ if (linkTypeName === "hyperlink" && !linkUrl) {
875
+ throw new Error("url is required for hyperlink links");
876
+ }
877
+ if (linkTypeName !== "hyperlink" && !linkToId) {
878
+ throw new Error("linkToId is required for work item links");
879
+ }
880
+ return {
881
+ op: "add",
882
+ path: "/relations/-",
883
+ value: {
884
+ rel: getLinkTypeFromName(linkTypeName),
885
+ url: linkTypeName === "hyperlink" ? linkUrl : `${orgUrl}/${resolvedProject}/_apis/wit/workItems/${linkToId}`,
886
+ attributes: { comment: linkComment || "" },
887
+ },
888
+ };
889
+ }),
813
890
  }));
814
891
  const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
815
892
  method: "PATCH",
package/dist/tools.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import { wrapExternalToolResponse } from "./shared/content-safety.js";
3
4
  import { Domain } from "./shared/domains.js";
4
5
  import { configureAdvSecTools } from "./tools/advanced-security.js";
5
6
  import { configureMcpAppsTools } from "./tools/mcp-apps.js";
@@ -14,11 +15,13 @@ import { configureWorkItemTools } from "./tools/work-items.js";
14
15
  function configureAllTools(server, tokenProvider, connectionProvider, userAgentProvider, enabledDomains) {
15
16
  const configureIfDomainEnabled = (domain, configureFn) => {
16
17
  if (enabledDomains.has(domain)) {
17
- configureFn();
18
+ configureToolsWithContentSafety(server, domain, configureFn);
18
19
  }
19
20
  };
20
21
  configureIfDomainEnabled(Domain.CORE, () => configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider));
21
- configureIfDomainEnabled(Domain.MCP_APPS, () => configureMcpAppsTools(server));
22
+ // This is a local health-check response and contains no Azure DevOps content.
23
+ if (enabledDomains.has(Domain.MCP_APPS))
24
+ configureMcpAppsTools(server);
22
25
  configureIfDomainEnabled(Domain.WORK, () => configureWorkTools(server, tokenProvider, connectionProvider));
23
26
  configureIfDomainEnabled(Domain.PIPELINES, () => configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider));
24
27
  configureIfDomainEnabled(Domain.REPOSITORIES, () => configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider));
@@ -28,4 +31,34 @@ function configureAllTools(server, tokenProvider, connectionProvider, userAgentP
28
31
  configureIfDomainEnabled(Domain.SEARCH, () => configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider));
29
32
  configureIfDomainEnabled(Domain.ADVANCED_SECURITY, () => configureAdvSecTools(server, tokenProvider, connectionProvider));
30
33
  }
34
+ /**
35
+ * Centralizes the untrusted-content boundary for tool responses. Tool registration
36
+ * is synchronous, so the original method is restored before this function returns.
37
+ */
38
+ function configureToolsWithContentSafety(server, domain, configureFn) {
39
+ const originalTool = server.tool;
40
+ const originalRegisterTool = server.registerTool;
41
+ const wrapRegistrationMethod = (registrationMethod) => new Proxy(registrationMethod, {
42
+ apply(target, thisArg, argumentsList) {
43
+ const callbackIndex = argumentsList.length - 1;
44
+ const callback = argumentsList[callbackIndex];
45
+ if (typeof callback === "function") {
46
+ argumentsList[callbackIndex] = async (...callbackArgs) => {
47
+ const response = (await Reflect.apply(callback, undefined, callbackArgs));
48
+ return wrapExternalToolResponse(response, `Azure DevOps ${domain}`);
49
+ };
50
+ }
51
+ return Reflect.apply(target, thisArg, argumentsList);
52
+ },
53
+ });
54
+ server.tool = wrapRegistrationMethod(originalTool);
55
+ server.registerTool = wrapRegistrationMethod(originalRegisterTool);
56
+ try {
57
+ configureFn();
58
+ }
59
+ finally {
60
+ server.tool = originalTool;
61
+ server.registerTool = originalRegisterTool;
62
+ }
63
+ }
31
64
  export { configureAllTools };
package/dist/useragent.js CHANGED
File without changes
package/dist/utils.js CHANGED
@@ -3,6 +3,19 @@
3
3
  export const apiVersion = "7.2-preview.1";
4
4
  export const batchApiVersion = "5.0";
5
5
  export const markdownCommentsApiVersion = "7.2-preview.4";
6
+ /**
7
+ * Returns the user-supplied CLI arguments.
8
+ *
9
+ * The server is always started script-style — `[runtime, scriptPath, ...args]` — on Node
10
+ * and on Electron hosts alike, so the first two entries are always dropped.
11
+ *
12
+ * Do not replace this with yargs' `hideBin`: it drops a single entry whenever
13
+ * `process.versions.electron` is set and `process.defaultApp` is not, so on an Electron
14
+ * host the script path survives and is parsed as the organization name.
15
+ */
16
+ export function getCliArgs(argv = process.argv) {
17
+ return argv.slice(2);
18
+ }
6
19
  export function createEnumMapping(enumObject) {
7
20
  const mapping = {};
8
21
  for (const [key, value] of Object.entries(enumObject)) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.9.0";
1
+ export const packageVersion = "2.10.0-nightly.20260910";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.9.0",
3
+ "version": "2.10.0-nightly.20260910",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",
@@ -37,12 +37,15 @@
37
37
  "test": "jest"
38
38
  },
39
39
  "dependencies": {
40
- "@azure/identity": "^4.10.0",
41
- "@azure/msal-node": "^5.0.6",
42
- "@modelcontextprotocol/sdk": "1.29.0",
40
+ "@azure/identity": "^4.13.0",
41
+ "@azure/logger": "^1.3.0",
42
+ "@azure/msal-node": "^5.5.0",
43
+ "@azure/msal-node-extensions": "^5.3.5",
44
+ "@modelcontextprotocol/sdk": "1.30.0",
43
45
  "azure-devops-extension-api": "^5.272.3",
44
46
  "azure-devops-extension-sdk": "^4.0.2",
45
47
  "azure-devops-node-api": "^15.1.2",
48
+ "open": "^10.2.0",
46
49
  "winston": "^3.18.3",
47
50
  "yargs": "^18.0.0",
48
51
  "zod": "^3.25.63",
@@ -69,5 +72,9 @@
69
72
  "**/*.(js|ts|jsx|tsx|json|css|md)": [
70
73
  "npm run format"
71
74
  ]
75
+ },
76
+ "allowScripts": {
77
+ "keytar@7.9.0": true,
78
+ "@azure/msal-node-extensions@5.3.5": true
72
79
  }
73
80
  }
@@ -1,34 +0,0 @@
1
- // Copyright (c) Microsoft Corporation.
2
- // Licensed under the MIT License.
3
- /** Builds an error `CallToolResult`. */
4
- export const errorResult = (text) => ({ content: [{ type: "text", text }], isError: true });
5
- /**
6
- * Routes a validated, action-carrying args object to the matching command.
7
- *
8
- * This is what removes long positional parameter lists from grouped ("action")
9
- * tools: instead of destructuring every possible field, the whole typed args
10
- * object is forwarded to the single command keyed by `args.action`, coupling
11
- * each action to exactly one command.
12
- *
13
- * - Unknown actions short-circuit with an "Unknown action" error and never
14
- * touch the context (so no connection is opened).
15
- * - Errors thrown by a command are caught and formatted using the optional
16
- * per-action `errorPrefixes` map (falling back to a generic message).
17
- * - Errors returned by a command (e.g. validation `errorResult`s) pass through
18
- * unchanged.
19
- */
20
- export async function dispatchAction(commands, context, args, errorPrefixes) {
21
- const command = commands[args.action];
22
- if (!command) {
23
- const supportedActions = Object.keys(commands).sort().join(", ");
24
- return errorResult(`Unknown action: ${args.action}. Supported actions: ${supportedActions}`);
25
- }
26
- try {
27
- return await command.execute(context, args);
28
- }
29
- catch (error) {
30
- const message = error instanceof Error ? error.message : "Unknown error occurred";
31
- const prefix = errorPrefixes?.[args.action];
32
- return errorResult(prefix ? `${prefix}${message}` : `Error: ${message}`);
33
- }
34
- }