@azure-devops/mcp 2.10.0-nightly.20260920 → 2.10.0-nightly.20260922

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
@@ -1,11 +1,8 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  import { AzureCliCredential, ChainedTokenCredential, DefaultAzureCredential } from "@azure/identity";
4
- import { PublicClientApplication } from "@azure/msal-node";
5
- import { NativeBrokerPlugin } from "@azure/msal-node-extensions";
6
- import open from "open";
7
4
  import { logger } from "./logger.js";
8
- const scopes = ["499b84ac-1321-427f-aa17-267ca6975798/.default"];
5
+ import { azureDevOpsScopes, OAuthAuthenticator } from "./oauth.js";
9
6
  const patAllowedHosts = new Set(["dev.azure.com", "vssps.dev.azure.com", "almsearch.dev.azure.com"]);
10
7
  function isPatAllowedHost(hostname) {
11
8
  const normalizedHostname = hostname.toLowerCase();
@@ -30,104 +27,6 @@ function installPatFetchInterceptor(basicValue) {
30
27
  return originalFetch(input, { ...init, headers });
31
28
  };
32
29
  }
33
- class OAuthAuthenticator {
34
- static clientId = "0d50963b-7bb9-4fe7-94c7-a99af00b5136";
35
- static defaultAuthority = "https://login.microsoftonline.com/common";
36
- static zeroTenantId = "00000000-0000-0000-0000-000000000000";
37
- accountId;
38
- publicClientApp;
39
- publicClientAppFallback;
40
- constructor(tenantId) {
41
- this.accountId = null;
42
- let authority = OAuthAuthenticator.defaultAuthority;
43
- if (tenantId && tenantId !== OAuthAuthenticator.zeroTenantId) {
44
- authority = `https://login.microsoftonline.com/${tenantId}`;
45
- logger.debug(`OAuthAuthenticator: Using tenant-specific authority for tenantId='${tenantId}'`);
46
- }
47
- else {
48
- logger.debug(`OAuthAuthenticator: Using default common authority`);
49
- }
50
- this.publicClientApp = new PublicClientApplication({
51
- auth: {
52
- clientId: OAuthAuthenticator.clientId,
53
- authority,
54
- },
55
- broker: {
56
- nativeBrokerPlugin: new NativeBrokerPlugin(),
57
- },
58
- system: {
59
- loggerOptions: {
60
- loggerCallback: (level, message) => {
61
- logger.debug(`MSALClient[${level}]: ${message}`);
62
- },
63
- },
64
- },
65
- });
66
- this.publicClientAppFallback = new PublicClientApplication({
67
- auth: {
68
- clientId: OAuthAuthenticator.clientId,
69
- authority,
70
- },
71
- });
72
- logger.debug(`OAuthAuthenticator: Initialized with clientId='${OAuthAuthenticator.clientId}'`);
73
- }
74
- async getToken() {
75
- let authResult = null;
76
- if (this.accountId) {
77
- logger.debug(`OAuthAuthenticator: Attempting silent token acquisition for cached account`);
78
- try {
79
- authResult = await this.publicClientApp.acquireTokenSilent({
80
- scopes,
81
- account: this.accountId,
82
- });
83
- logger.debug(`OAuthAuthenticator: Successfully acquired token silently`);
84
- }
85
- catch (error) {
86
- logger.debug(`OAuthAuthenticator: Silent token acquisition failed: ${error instanceof Error ? error.message : String(error)}`);
87
- authResult = null;
88
- }
89
- }
90
- else {
91
- logger.debug(`OAuthAuthenticator: No cached account available, interactive auth required`);
92
- }
93
- if (!authResult) {
94
- logger.debug(`OAuthAuthenticator: Starting interactive token acquisition`);
95
- try {
96
- authResult = await this.publicClientApp.acquireTokenInteractive({
97
- scopes,
98
- openBrowser: async (url) => {
99
- logger.debug(`OAuthAuthenticator: Opening browser for authentication with target URL: ${url}`);
100
- open(url);
101
- },
102
- });
103
- this.accountId = authResult.account;
104
- logger.debug(`OAuthAuthenticator: Successfully acquired token interactively, account cached`);
105
- }
106
- catch (error) {
107
- const msalErrorMessage = error.platformBrokerError ? JSON.stringify(error.platformBrokerError) : "";
108
- logger.debug(`OAuthAuthenticator: Interactive token acquisition failed: ${error instanceof Error ? error.message + msalErrorMessage : String(error)}`);
109
- authResult = null;
110
- }
111
- }
112
- if (!authResult) {
113
- logger.debug(`OAuthAuthenticator: Starting interactive token acquisition without broker`);
114
- authResult = await this.publicClientAppFallback.acquireTokenInteractive({
115
- scopes,
116
- openBrowser: async (url) => {
117
- logger.debug(`OAuthAuthenticator: Opening browser for authentication with target URL: ${url}`);
118
- open(url);
119
- },
120
- });
121
- logger.debug(`OAuthAuthenticator: Successfully acquired token interactively without broker`);
122
- }
123
- if (!authResult?.accessToken) {
124
- logger.error(`OAuthAuthenticator: Authentication result contains no access token`);
125
- throw new Error("Failed to obtain Azure DevOps OAuth token.");
126
- }
127
- logger.debug(`OAuthAuthenticator: Token obtained successfully`);
128
- return authResult.accessToken;
129
- }
130
- }
131
30
  function createAuthenticator(type, tenantId) {
132
31
  logger.debug(`Creating authenticator of type '${type}' with tenantId='${tenantId ?? "undefined"}'`);
133
32
  switch (type) {
@@ -170,7 +69,7 @@ function createAuthenticator(type, tenantId) {
170
69
  credential = new ChainedTokenCredential(azureCliCredential, credential);
171
70
  }
172
71
  return async () => {
173
- const result = await credential.getToken(scopes);
72
+ const result = await credential.getToken(azureDevOpsScopes);
174
73
  if (!result) {
175
74
  logger.error(`${type}: Failed to obtain token - credential.getToken returned null/undefined`);
176
75
  throw new Error("Failed to obtain Azure DevOps token. Ensure you have Azure CLI logged or use interactive type of authentication.");
package/dist/oauth.js ADDED
@@ -0,0 +1,130 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { PublicClientApplication } from "@azure/msal-node";
4
+ import open from "open";
5
+ import { logger } from "./logger.js";
6
+ export const azureDevOpsScopes = ["499b84ac-1321-427f-aa17-267ca6975798/.default"];
7
+ const clientId = "0d50963b-7bb9-4fe7-94c7-a99af00b5136";
8
+ const defaultAuthority = "https://login.microsoftonline.com/common";
9
+ const zeroTenantId = "00000000-0000-0000-0000-000000000000";
10
+ class ClientAppWrapper {
11
+ accountId;
12
+ publicClientApp;
13
+ withBroker;
14
+ constructor(authority) {
15
+ this.accountId = null;
16
+ this.publicClientApp = new PublicClientApplication({
17
+ auth: {
18
+ clientId,
19
+ authority,
20
+ },
21
+ });
22
+ this.withBroker = false;
23
+ }
24
+ static async initialize(authority, tryWithBroker) {
25
+ const clientApp = new ClientAppWrapper(authority);
26
+ if (tryWithBroker) {
27
+ try {
28
+ const { NativeBrokerPlugin } = await import("@azure/msal-node-extensions");
29
+ clientApp.publicClientApp = new PublicClientApplication({
30
+ auth: {
31
+ clientId,
32
+ authority,
33
+ },
34
+ broker: {
35
+ nativeBrokerPlugin: new NativeBrokerPlugin(),
36
+ },
37
+ system: {
38
+ loggerOptions: {
39
+ loggerCallback: (level, message) => {
40
+ logger.debug(`MSALClient[${level}]: ${message}`);
41
+ },
42
+ },
43
+ },
44
+ });
45
+ clientApp.withBroker = true;
46
+ logger.debug(`PublicClientApplication(authority='${authority}') initialized with broker successfully`);
47
+ }
48
+ catch (error) {
49
+ logger.debug(`PublicClientApplication(authority='${authority}') initialized without broker`);
50
+ logger.error(`Failed to initialize PublicClientApplication with broker: ${error instanceof Error ? error.message : String(error)}`);
51
+ }
52
+ }
53
+ return clientApp;
54
+ }
55
+ isWithBroker() {
56
+ return this.withBroker;
57
+ }
58
+ async getToken() {
59
+ const logPrefix = `ClientAppWrapper(${this.withBroker ? "withBroker" : "noBroker"})`;
60
+ let authResult = null;
61
+ if (this.accountId) {
62
+ logger.debug(`${logPrefix}: Attempting silent token acquisition for cached account`);
63
+ try {
64
+ authResult = await this.publicClientApp.acquireTokenSilent({
65
+ scopes: azureDevOpsScopes,
66
+ account: this.accountId,
67
+ });
68
+ logger.debug(`${logPrefix}: Successfully acquired token silently`);
69
+ }
70
+ catch (error) {
71
+ logger.debug(`${logPrefix}: Silent token acquisition failed: ${error instanceof Error ? error.message : String(error)}`);
72
+ authResult = null;
73
+ }
74
+ }
75
+ else {
76
+ logger.debug(`${logPrefix}: No cached account available, interactive auth required`);
77
+ }
78
+ if (!authResult) {
79
+ logger.debug(`${logPrefix}: Starting interactive token acquisition`);
80
+ try {
81
+ authResult = await this.publicClientApp.acquireTokenInteractive({
82
+ scopes: azureDevOpsScopes,
83
+ openBrowser: async (url) => {
84
+ logger.debug(`${logPrefix}: Opening browser for authentication with target URL: ${url}`);
85
+ open(url);
86
+ },
87
+ });
88
+ this.accountId = authResult.account;
89
+ logger.debug(`${logPrefix}: Successfully acquired token interactively, account cached`);
90
+ }
91
+ catch (error) {
92
+ const msalErrorMessage = error.platformBrokerError ? JSON.stringify(error.platformBrokerError) : "";
93
+ logger.debug(`${logPrefix}: Interactive token acquisition failed: ${error instanceof Error ? error.message + msalErrorMessage : String(error)}`);
94
+ authResult = null;
95
+ }
96
+ }
97
+ if (!authResult?.accessToken) {
98
+ throw new Error("Failed to obtain Azure DevOps OAuth token.");
99
+ }
100
+ logger.debug(`${logPrefix}: Token obtained successfully`);
101
+ return authResult.accessToken;
102
+ }
103
+ }
104
+ export class OAuthAuthenticator {
105
+ authority;
106
+ clientApp;
107
+ constructor(tenantId) {
108
+ this.authority = defaultAuthority;
109
+ if (tenantId && tenantId !== zeroTenantId) {
110
+ this.authority = `https://login.microsoftonline.com/${tenantId}`;
111
+ }
112
+ this.clientApp = null;
113
+ }
114
+ async getToken() {
115
+ if (!this.clientApp) {
116
+ this.clientApp = await ClientAppWrapper.initialize(this.authority, true);
117
+ }
118
+ try {
119
+ const token = await this.clientApp.getToken();
120
+ return token;
121
+ }
122
+ catch {
123
+ if (this.clientApp.isWithBroker()) {
124
+ logger.debug(`OAuthAuthenticator: Token acquisition failed with broker, retrying without broker`);
125
+ this.clientApp = await ClientAppWrapper.initialize(this.authority, false);
126
+ }
127
+ }
128
+ return await this.clientApp.getToken();
129
+ }
130
+ }
@@ -178,7 +178,7 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
178
178
  catch { }
179
179
  }
180
180
  }
181
- if (!pageContent) {
181
+ if (pageContent === undefined) {
182
182
  if (!resolvedPath) {
183
183
  resolvedPath = "/";
184
184
  }
@@ -108,13 +108,14 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
108
108
  .optional()
109
109
  .describe("Expand options (None, Fields, Relations, Links, All). Used for: get, list_revisions. For get, cannot be combined with fields."),
110
110
  top: z.coerce.number().optional().describe("Maximum number of results to return. Used for: list_comments, my, list_revisions. Defaults vary by action."),
111
+ continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list_comments."),
111
112
  includeCompleted: z.boolean().optional().default(false).describe("Include completed work items. Used for: my. Defaults to false."),
112
113
  type: z.enum(["assignedtome", "myactivity"]).optional().describe("Type of work items to retrieve. Used for: my. Defaults to 'assignedtome'."),
113
114
  skip: z.coerce.number().optional().describe("Number of results to skip for pagination. Used for: list_revisions."),
114
115
  team: z.string().optional().describe("Team name or ID. Used for: list_for_iteration."),
115
116
  iterationId: z.string().optional().describe("Iteration ID. Required for: list_for_iteration."),
116
117
  workItemType: z.string().optional().describe("Work item type name. Required for: get_type."),
117
- }, async ({ action, project, id, ids, workItemId, fields, asOf, expand, top, includeCompleted, type, skip, team, iterationId, workItemType }) => {
118
+ }, async ({ action, project, id, ids, workItemId, fields, asOf, expand, top, continuationToken, includeCompleted, type, skip, team, iterationId, workItemType }) => {
118
119
  try {
119
120
  const connection = await connectionProvider();
120
121
  let resolvedProject = project;
@@ -177,7 +178,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
177
178
  resolvedProject = result.resolved;
178
179
  }
179
180
  const workItemApi = await connection.getWorkItemTrackingApi();
180
- const comments = await workItemApi.getComments(resolvedProject, workItemId, top ?? 50);
181
+ const comments = await workItemApi.getComments(resolvedProject, workItemId, top ?? 50, continuationToken);
181
182
  return { content: [{ type: "text", text: JSON.stringify(comments, null, 2) }] };
182
183
  }
183
184
  if (action === "my") {
@@ -286,7 +287,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
286
287
  team: z.string().optional().describe("Team name or ID. Used for: get_results, wiql."),
287
288
  timePrecision: z.boolean().optional().describe("Include time precision in date fields. Used for: get_results, wiql."),
288
289
  top: z.coerce.number().default(50).describe("Maximum number of results to return. Used for: get_results, wiql. Defaults to 50."),
289
- responseType: z.enum(["full", "ids"]).default("full").describe("Response type: 'full' returns complete results (default), 'ids' returns only work item IDs. Used for: get_results."),
290
+ responseType: z
291
+ .enum(["full", "ids"])
292
+ .default("full")
293
+ .describe("Response type: 'full' returns complete results (default). 'ids' returns only work item IDs and supports flat queries only; any other query type is not supported. Used for: get_results."),
290
294
  wiql: z.string().max(32768).optional().describe('The WIQL query string to execute. Required for: wiql. Example: "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @project".'),
291
295
  }, async ({ action, project, query, expand, depth, includeDeleted, useIsoDateFormat, id, team, timePrecision, top, responseType, wiql }) => {
292
296
  try {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.10.0-nightly.20260920";
1
+ export const packageVersion = "2.10.0-nightly.20260922";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.10.0-nightly.20260920",
3
+ "version": "2.10.0-nightly.20260922",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",
@@ -40,7 +40,6 @@
40
40
  "@azure/identity": "^4.13.0",
41
41
  "@azure/logger": "^1.3.0",
42
42
  "@azure/msal-node": "^5.5.0",
43
- "@azure/msal-node-extensions": "^5.3.5",
44
43
  "@modelcontextprotocol/sdk": "1.30.0",
45
44
  "azure-devops-extension-api": "^5.272.3",
46
45
  "azure-devops-extension-sdk": "^4.0.2",
@@ -51,6 +50,9 @@
51
50
  "zod": "^3.25.63",
52
51
  "zod-to-json-schema": "^3.24.5"
53
52
  },
53
+ "optionalDependencies": {
54
+ "@azure/msal-node-extensions": "^5.3.5"
55
+ },
54
56
  "devDependencies": {
55
57
  "@types/jest": "^30.0.0",
56
58
  "@types/node": "^22.19.1",