@azure-devops/mcp 2.5.0-nightly.20260414 → 2.5.0-nightly.20260416

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/dist/auth.js CHANGED
@@ -71,6 +71,19 @@ class OAuthAuthenticator {
71
71
  function createAuthenticator(type, tenantId) {
72
72
  logger.debug(`Creating authenticator of type '${type}' with tenantId='${tenantId ?? "undefined"}'`);
73
73
  switch (type) {
74
+ case "pat":
75
+ logger.debug(`Authenticator: Using PAT authentication (PERSONAL_ACCESS_TOKEN)`);
76
+ return async () => {
77
+ logger.debug(`${type}: Reading token from PERSONAL_ACCESS_TOKEN environment variable`);
78
+ const b64Pat = process.env["PERSONAL_ACCESS_TOKEN"];
79
+ if (!b64Pat) {
80
+ logger.error(`${type}: PERSONAL_ACCESS_TOKEN environment variable is not set or empty`);
81
+ throw new Error("Environment variable 'PERSONAL_ACCESS_TOKEN' is not set or empty. Please set it with a valid base64-encoded Azure DevOps Personal Access Token.");
82
+ }
83
+ // Return base64 value as-is — caller uses it directly as the Basic auth credential
84
+ logger.debug(`${type}: Successfully retrieved PAT from environment variable`);
85
+ return b64Pat;
86
+ };
74
87
  case "envvar":
75
88
  logger.debug(`Authenticator: Using environment variable authentication (ADO_MCP_AUTH_TOKEN)`);
76
89
  // Read token from fixed environment variable
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // Licensed under the MIT License.
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { getBearerHandler, WebApi } from "azure-devops-node-api";
6
+ import { getBearerHandler, getPersonalAccessTokenHandler, WebApi } from "azure-devops-node-api";
7
7
  import yargs from "yargs";
8
8
  import { hideBin } from "yargs/helpers";
9
9
  import { createAuthenticator } from "./auth.js";
@@ -41,7 +41,7 @@ const argv = yargs(hideBin(process.argv))
41
41
  alias: "a",
42
42
  describe: "Type of authentication to use",
43
43
  type: "string",
44
- choices: ["interactive", "azcli", "env", "envvar"],
44
+ choices: ["interactive", "azcli", "env", "envvar", "pat"],
45
45
  default: defaultAuthenticationType,
46
46
  })
47
47
  .option("tenant", {
@@ -55,10 +55,12 @@ export const orgName = argv.organization;
55
55
  const orgUrl = "https://dev.azure.com/" + orgName;
56
56
  const domainsManager = new DomainsManager(argv.domains);
57
57
  export const enabledDomains = domainsManager.getEnabledDomains();
58
- function getAzureDevOpsClient(getAzureDevOpsToken, userAgentComposer) {
58
+ function getAzureDevOpsClient(getAzureDevOpsToken, userAgentComposer, authType) {
59
59
  return async () => {
60
60
  const accessToken = await getAzureDevOpsToken();
61
- const authHandler = getBearerHandler(accessToken);
61
+ // For pat, accessToken is base64("{email}:{token}"). Decode to extract the token part,
62
+ // since getPersonalAccessTokenHandler prepends ":" internally and just needs the raw token.
63
+ const authHandler = authType === "pat" ? getPersonalAccessTokenHandler(Buffer.from(accessToken, "base64").toString("utf8").split(":").slice(1).join(":")) : getBearerHandler(accessToken);
62
64
  const connection = new WebApi(orgUrl, authHandler, undefined, {
63
65
  productName: "AzureDevOps.MCP",
64
66
  productVersion: packageVersion,
@@ -93,9 +95,25 @@ async function main() {
93
95
  };
94
96
  const tenantId = (await getOrgTenant(orgName)) ?? argv.tenant;
95
97
  const authenticator = createAuthenticator(argv.authentication, tenantId);
98
+ if (argv.authentication === "pat") {
99
+ const basicValue = await authenticator();
100
+ // basicValue is already base64("{email}:{token}") — use it directly in the Authorization header
101
+ const _originalFetch = globalThis.fetch;
102
+ globalThis.fetch = async (input, init) => {
103
+ if (init?.headers) {
104
+ const headers = new Headers(init.headers);
105
+ if (headers.get("Authorization")?.startsWith("Bearer ")) {
106
+ headers.set("Authorization", `Basic ${basicValue}`);
107
+ init = { ...init, headers };
108
+ }
109
+ }
110
+ return _originalFetch(input, init);
111
+ };
112
+ logger.debug("PAT mode: global fetch interceptor installed to rewrite Bearer -> Basic auth headers");
113
+ }
96
114
  // removing prompts untill further notice
97
115
  // configurePrompts(server);
98
- configureAllTools(server, authenticator, getAzureDevOpsClient(authenticator, userAgentComposer), () => userAgentComposer.userAgent, enabledDomains);
116
+ configureAllTools(server, authenticator, getAzureDevOpsClient(authenticator, userAgentComposer, argv.authentication), () => userAgentComposer.userAgent, enabledDomains);
99
117
  const transport = new StdioServerTransport();
100
118
  await server.connect(transport);
101
119
  }
@@ -293,7 +293,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
293
293
  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."),
294
294
  workItemId: z.coerce.number().min(1).describe("The ID of the work item to add a comment to."),
295
295
  comment: z.string().describe("The text of the comment to add to the work item."),
296
- format: z.enum(["markdown", "html"]).optional().default("html"),
296
+ format: z.enum(["Markdown", "Html"]).optional().default("Markdown").describe("The format of the comment text, e.g., 'Markdown', 'Html'. Optional, defaults to 'Markdown'."),
297
297
  }, async ({ project, workItemId, comment, format }) => {
298
298
  try {
299
299
  const connection = await connectionProvider();
@@ -309,7 +309,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
309
309
  const body = {
310
310
  text: comment,
311
311
  };
312
- const formatParameter = format === "markdown" ? 0 : 1;
312
+ const formatParameter = (format ?? "Markdown") === "Markdown" ? 0 : 1;
313
313
  const response = await fetch(`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
314
314
  method: "POST",
315
315
  headers: {
@@ -340,7 +340,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
340
340
  workItemId: z.coerce.number().min(1).describe("The ID of the work item."),
341
341
  commentId: z.coerce.number().min(1).describe("The ID of the comment to update."),
342
342
  text: z.string().describe("The updated comment text."),
343
- format: z.enum(["markdown", "html"]).optional().default("html"),
343
+ format: z.enum(["Markdown", "Html"]).optional().default("Markdown").describe("The format of the comment text, e.g., 'Markdown', 'Html'. Optional, defaults to 'Markdown'."),
344
344
  }, async ({ project, workItemId, commentId, text, format }) => {
345
345
  try {
346
346
  const connection = await connectionProvider();
@@ -354,7 +354,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
354
354
  const orgUrl = connection.serverUrl;
355
355
  const accessToken = await tokenProvider();
356
356
  const body = { text };
357
- const formatParameter = format === "markdown" ? 0 : 1;
357
+ const formatParameter = (format ?? "Markdown") === "Markdown" ? 0 : 1;
358
358
  const response = await fetch(`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments/${commentId}?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
359
359
  method: "PATCH",
360
360
  headers: {
@@ -446,7 +446,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
446
446
  items: z.array(z.object({
447
447
  title: z.string().describe("The title of the child work item."),
448
448
  description: z.string().describe("The description of the child work item."),
449
- format: z.enum(["Markdown", "Html"]).default("Html").describe("Format for the description on the child work item, e.g., 'Markdown', 'Html'. Defaults to 'Html'."),
449
+ format: z.enum(["Markdown", "Html"]).default("Markdown").describe("Format for the description on the child work item, e.g., 'Markdown', 'Html'. Defaults to 'Markdown'."),
450
450
  areaPath: z.string().optional().describe("Optional area path for the child work item."),
451
451
  iterationPath: z.string().optional().describe("Optional iteration path for the child work item."),
452
452
  })),
@@ -714,7 +714,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
714
714
  .array(z.object({
715
715
  name: z.string().describe("The name of the field, e.g., 'System.Title'."),
716
716
  value: z.string().describe("The value of the field."),
717
- format: z.enum(["Html", "Markdown"]).optional().describe("the format of the field value, e.g., 'Html', 'Markdown'. Optional, defaults to 'Html'."),
717
+ format: z.enum(["Html", "Markdown"]).optional().default("Markdown").describe("the format of the field value, e.g., 'Html', 'Markdown'. Optional, defaults to 'Markdown'."),
718
718
  }))
719
719
  .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."),
720
720
  }, async ({ project, workItemType, fields }) => {
@@ -835,7 +835,11 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
835
835
  id: z.coerce.number().min(1).describe("The ID of the work item to update."),
836
836
  path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
837
837
  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."),
838
- 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'."),
838
+ format: z
839
+ .enum(["Html", "Markdown"])
840
+ .optional()
841
+ .default("Markdown")
842
+ .describe("The format of the field value. Only to be used for large text fields. e.g., 'Html', 'Markdown'. Optional, defaults to 'Markdown'."),
839
843
  }))
840
844
  .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)."),
841
845
  }, async ({ updates }) => {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.5.0-nightly.20260414";
1
+ export const packageVersion = "2.5.0-nightly.20260416";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.5.0-nightly.20260414",
3
+ "version": "2.5.0-nightly.20260416",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",
@@ -59,7 +59,7 @@
59
59
  "jest": "^30.0.2",
60
60
  "jest-extended": "^7.0.0",
61
61
  "lint-staged": "^16.2.7",
62
- "prettier": "3.8.2",
62
+ "prettier": "3.8.3",
63
63
  "shx": "^0.4.0",
64
64
  "ts-jest": "^29.4.6",
65
65
  "tsconfig-paths": "^4.2.0",