@azure-devops/mcp 2.10.0-nightly.20260930 → 2.10.0-nightly.20261002

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 CHANGED
@@ -1,19 +1,14 @@
1
1
  # Azure DevOps MCP Server
2
2
 
3
- > [!WARNING]
4
- > We recently completed a full tool consolidation that includes renaming of existing tools. Please see the [Toolset documentation](docs/TOOLSET.md) for the complete list of new tool names.
3
+ > [!IMPORTANT]
4
+ > We recommend using the [Remote MCP Server](https://learn.microsoft.com/en-us/azure/devops/mcp-server/remote-mcp-server) instead of this local server. It requires no installation and gets new features first.
5
5
  >
6
- > If this is a breaking change for your agents or skills, you can temporarily pin the version to `@azure-devops/mcp@2.8.1`
6
+ > [Learn more](#remote-mcp-server-recommended)
7
7
 
8
8
  This project gives AI agents access to Azure DevOps through the Model Context Protocol (MCP). Use the hosted remote server for the simplest setup, or run the local server when you need a `stdio` connection.
9
9
 
10
10
  ## Table of Contents
11
11
 
12
- > [!IMPORTANT]
13
- > We recommend using the [Remote MCP Server](https://learn.microsoft.com/en-us/azure/devops/mcp-server/remote-mcp-server) instead of this local server. It requires no installation and gets new features first.
14
- >
15
- > [Learn more](#remote-mcp-server-recommended)
16
-
17
12
  1. [Overview](#overview)
18
13
  2. [Design](#design)
19
14
  3. [Remote MCP Server (Recommended)](#remote-mcp-server-recommended)
@@ -3,6 +3,8 @@
3
3
  import { apiVersion, getEnumKeys, safeEnumConvert } from "../utils.js";
4
4
  import { BuildQueryOrder, DefinitionQueryOrder } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
5
5
  import { z } from "zod";
6
+ const buildQueryOrderKeys = getEnumKeys(BuildQueryOrder);
7
+ const definitionQueryOrderKeys = getEnumKeys(DefinitionQueryOrder);
6
8
  import { StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
7
9
  import { ConfigurationType, RepositoryType } from "azure-devops-node-api/interfaces/PipelinesInterfaces.js";
8
10
  import { mkdirSync, createWriteStream } from "fs";
@@ -153,7 +155,7 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
153
155
  continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list, get_changes."),
154
156
  maxBuildsPerDefinition: z.number().optional().describe("Maximum number of builds per definition. Used for: list."),
155
157
  deletedFilter: z.number().optional().describe("Filter for deleted builds (see QueryDeletedOption enum). Used for: list."),
156
- queryOrder: z.string().optional().describe("Order in which builds are returned (BuildQueryOrder values). Used for: list."),
158
+ queryOrder: z.enum(buildQueryOrderKeys).optional().describe("Order in which builds are returned. Used for: list."),
157
159
  branchName: z.string().optional().describe("Branch name to filter builds. Used for: list."),
158
160
  buildIds: z.array(z.coerce.number().min(1)).optional().describe("Array of specific build IDs to retrieve. Used for: list."),
159
161
  repositoryId: z.string().optional().describe("Repository ID to filter builds. Used for: list."),
@@ -237,7 +239,7 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
237
239
  repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Repository type to filter definitions. Used for: list."),
238
240
  name: z.string().optional().describe("Name filter for build definitions. Used for: list."),
239
241
  path: z.string().optional().describe("Path filter for build definitions. Used for: list."),
240
- queryOrder: z.string().optional().describe("Order in which definitions are returned (DefinitionQueryOrder values). Used for: list."),
242
+ queryOrder: z.enum(definitionQueryOrderKeys).optional().describe("Order in which definitions are returned. Used for: list."),
241
243
  top: z.number().optional().describe("Maximum number of definitions to return. Used for: list."),
242
244
  continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list."),
243
245
  minMetricsTime: z.coerce.date().optional().describe("Minimum metrics time to filter definitions. Used for: list."),
@@ -899,8 +899,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
899
899
  fullResponse: z.boolean().optional().default(false).describe("Return full JSON response. Used for reply and update."),
900
900
  rightFileStartLine: z.coerce.number().min(1).optional().describe("Start line in the right file. Used for create."),
901
901
  rightFileStartOffset: z.number().optional().describe("Start character offset in the right file. Used for create."),
902
- rightFileEndLine: z.number().optional().describe("End line in the right file. Used for create."),
903
- rightFileEndOffset: z.number().optional().describe("End character offset in the right file. Used for create."),
902
+ rightFileEndLine: z.number().optional().describe("End line in the right file. Used for create. Must be supplied together with rightFileEndOffset."),
903
+ rightFileEndOffset: z.number().optional().describe("End character offset in the right file. Used for create. Must be supplied together with rightFileEndLine."),
904
904
  changeTrackingId: z.coerce.number().int().min(1).optional().describe("The file change tracking ID from the pull request iteration changes. Used for create."),
905
905
  firstComparingIteration: z.coerce.number().int().min(0).optional().describe("The iteration on the left side of the diff. Used for create."),
906
906
  secondComparingIteration: z.coerce.number().int().min(1).optional().describe("The iteration on the right side of the diff. Used for create."),
@@ -346,18 +346,18 @@ function parseWikiUrl(url) {
346
346
  if (idx < 1 || segments[idx + 1] !== "wikis") {
347
347
  return { error: "URL does not match expected wiki pattern (missing /_wiki/wikis/ segment)." };
348
348
  }
349
- const project = segments[idx - 1];
350
- const wikiIdentifier = segments[idx + 2];
351
- if (!project || !wikiIdentifier) {
349
+ const encodedProject = segments[idx - 1];
350
+ const encodedWikiIdentifier = segments[idx + 2];
351
+ if (!encodedProject || !encodedWikiIdentifier) {
352
352
  return { error: "Could not extract project or wikiIdentifier from URL." };
353
353
  }
354
+ const project = decodeURIComponent(encodedProject);
355
+ const wikiIdentifier = decodeURIComponent(encodedWikiIdentifier);
354
356
  // Query form with pagePath
355
357
  const pagePathParam = u.searchParams.get("pagePath");
356
358
  if (pagePathParam) {
357
- let decoded = decodeURIComponent(pagePathParam);
358
- if (!decoded.startsWith("/"))
359
- decoded = "/" + decoded;
360
- return { project, wikiIdentifier, pagePath: decoded };
359
+ const pagePath = pagePathParam.startsWith("/") ? pagePathParam : `/${pagePathParam}`;
360
+ return { project, wikiIdentifier, pagePath };
361
361
  }
362
362
  // Path ID form: .../wikis/{wikiIdentifier}/{pageId}/...
363
363
  const afterWiki = segments.slice(idx + 3); // elements after wikiIdentifier
@@ -71,10 +71,8 @@ function escapeHtml(value) {
71
71
  };
72
72
  return value.replace(/[&<>"']/g, (character) => entities[character]);
73
73
  }
74
- async function resolveCommentMentions(text, format, tokenProvider, connectionProvider, userAgentProvider) {
74
+ async function prepareCommentText(text, format, tokenProvider, connectionProvider, userAgentProvider) {
75
75
  const emailMatches = [...text.matchAll(/@<([^<>\s]+@[^<>\s]+)>/g)];
76
- if (emailMatches.length === 0)
77
- return text;
78
76
  const identities = new Map();
79
77
  for (const email of new Set(emailMatches.map((match) => match[1]))) {
80
78
  try {
@@ -84,11 +82,18 @@ async function resolveCommentMentions(text, format, tokenProvider, connectionPro
84
82
  // Leave mentions unchanged when their identities cannot be resolved.
85
83
  }
86
84
  }
87
- return text.replace(/@<([^<>\s]+@[^<>\s]+)>/g, (mention, email) => {
85
+ // Encode Markdown transport text once, preserving mention tokens for Azure DevOps.
86
+ // Encoding ampersands also preserves intentional entity text after one decode.
87
+ const markdown = format === "Markdown" || format === undefined;
88
+ 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) => {
89
+ if (id)
90
+ return mention;
91
+ if (!email)
92
+ return markdown ? escapeHtml(mention) : mention;
88
93
  const identity = identities.get(email);
89
94
  if (!identity)
90
95
  return escapeHtml(mention);
91
- return format === "Markdown" || format === undefined ? `@<${identity.id}>` : `<a href="#" data-vss-mention="version:2.0,${identity.id}">@${escapeHtml(identity.displayName)}</a>`;
96
+ return markdown ? `@<${identity.id}>` : `<a href="#" data-vss-mention="version:2.0,${identity.id}">@${escapeHtml(identity.displayName)}</a>`;
92
97
  });
93
98
  }
94
99
  function configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider) {
@@ -841,7 +846,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
841
846
  const orgUrl = connection.serverUrl;
842
847
  const accessToken = await tokenProvider();
843
848
  const formatParameter = (format ?? "Markdown") === "Markdown" ? 0 : 1;
844
- const resolvedText = await resolveCommentMentions(text, format, tokenProvider, connectionProvider, userAgentProvider);
849
+ const resolvedText = await prepareCommentText(text, format, tokenProvider, connectionProvider, userAgentProvider);
845
850
  if (action === "add") {
846
851
  const response = await fetch(`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
847
852
  method: "POST",
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.10.0-nightly.20260930";
1
+ export const packageVersion = "2.10.0-nightly.20261002";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.10.0-nightly.20260930",
3
+ "version": "2.10.0-nightly.20261002",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",