@azure-devops/mcp 2.2.2-nightly.20251121 → 2.2.2-nightly.20251125
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 +27 -1
- package/dist/index.js +12 -1
- package/dist/logger.js +34 -0
- package/dist/org-tenants.js +4 -3
- package/dist/shared/domains.js +2 -1
- package/dist/tools/pipelines.js +2 -2
- package/dist/tools/repositories.js +29 -4
- package/dist/version.js +1 -1
- package/package.json +3 -2
package/dist/auth.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { AzureCliCredential, ChainedTokenCredential, DefaultAzureCredential } from "@azure/identity";
|
|
4
4
|
import { PublicClientApplication } from "@azure/msal-node";
|
|
5
5
|
import open from "open";
|
|
6
|
+
import { logger } from "./logger.js";
|
|
6
7
|
const scopes = ["499b84ac-1321-427f-aa17-267ca6975798/.default"];
|
|
7
8
|
class OAuthAuthenticator {
|
|
8
9
|
static clientId = "0d50963b-7bb9-4fe7-94c7-a99af00b5136";
|
|
@@ -15,6 +16,10 @@ class OAuthAuthenticator {
|
|
|
15
16
|
let authority = OAuthAuthenticator.defaultAuthority;
|
|
16
17
|
if (tenantId && tenantId !== OAuthAuthenticator.zeroTenantId) {
|
|
17
18
|
authority = `https://login.microsoftonline.com/${tenantId}`;
|
|
19
|
+
logger.debug(`OAuthAuthenticator: Using tenant-specific authority for tenantId='${tenantId}'`);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
logger.debug(`OAuthAuthenticator: Using default common authority`);
|
|
18
23
|
}
|
|
19
24
|
this.publicClientApp = new PublicClientApplication({
|
|
20
25
|
auth: {
|
|
@@ -22,49 +27,67 @@ class OAuthAuthenticator {
|
|
|
22
27
|
authority,
|
|
23
28
|
},
|
|
24
29
|
});
|
|
30
|
+
logger.debug(`OAuthAuthenticator: Initialized with clientId='${OAuthAuthenticator.clientId}'`);
|
|
25
31
|
}
|
|
26
32
|
async getToken() {
|
|
27
33
|
let authResult = null;
|
|
28
34
|
if (this.accountId) {
|
|
35
|
+
logger.debug(`OAuthAuthenticator: Attempting silent token acquisition for cached account`);
|
|
29
36
|
try {
|
|
30
37
|
authResult = await this.publicClientApp.acquireTokenSilent({
|
|
31
38
|
scopes,
|
|
32
39
|
account: this.accountId,
|
|
33
40
|
});
|
|
41
|
+
logger.debug(`OAuthAuthenticator: Successfully acquired token silently`);
|
|
34
42
|
}
|
|
35
|
-
catch {
|
|
43
|
+
catch (error) {
|
|
44
|
+
logger.debug(`OAuthAuthenticator: Silent token acquisition failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
36
45
|
authResult = null;
|
|
37
46
|
}
|
|
38
47
|
}
|
|
48
|
+
else {
|
|
49
|
+
logger.debug(`OAuthAuthenticator: No cached account available, interactive auth required`);
|
|
50
|
+
}
|
|
39
51
|
if (!authResult) {
|
|
52
|
+
logger.debug(`OAuthAuthenticator: Starting interactive token acquisition`);
|
|
40
53
|
authResult = await this.publicClientApp.acquireTokenInteractive({
|
|
41
54
|
scopes,
|
|
42
55
|
openBrowser: async (url) => {
|
|
56
|
+
logger.debug(`OAuthAuthenticator: Opening browser for authentication`);
|
|
43
57
|
open(url);
|
|
44
58
|
},
|
|
45
59
|
});
|
|
46
60
|
this.accountId = authResult.account;
|
|
61
|
+
logger.debug(`OAuthAuthenticator: Successfully acquired token interactively, account cached`);
|
|
47
62
|
}
|
|
48
63
|
if (!authResult.accessToken) {
|
|
64
|
+
logger.error(`OAuthAuthenticator: Authentication result contains no access token`);
|
|
49
65
|
throw new Error("Failed to obtain Azure DevOps OAuth token.");
|
|
50
66
|
}
|
|
67
|
+
logger.debug(`OAuthAuthenticator: Token obtained successfully`);
|
|
51
68
|
return authResult.accessToken;
|
|
52
69
|
}
|
|
53
70
|
}
|
|
54
71
|
function createAuthenticator(type, tenantId) {
|
|
72
|
+
logger.debug(`Creating authenticator of type '${type}' with tenantId='${tenantId ?? "undefined"}'`);
|
|
55
73
|
switch (type) {
|
|
56
74
|
case "envvar":
|
|
75
|
+
logger.debug(`Authenticator: Using environment variable authentication (ADO_MCP_AUTH_TOKEN)`);
|
|
57
76
|
// Read token from fixed environment variable
|
|
58
77
|
return async () => {
|
|
78
|
+
logger.debug(`${type}: Reading token from ADO_MCP_AUTH_TOKEN environment variable`);
|
|
59
79
|
const token = process.env["ADO_MCP_AUTH_TOKEN"];
|
|
60
80
|
if (!token) {
|
|
81
|
+
logger.error(`${type}: ADO_MCP_AUTH_TOKEN environment variable is not set or empty`);
|
|
61
82
|
throw new Error("Environment variable 'ADO_MCP_AUTH_TOKEN' is not set or empty. Please set it with a valid Azure DevOps Personal Access Token.");
|
|
62
83
|
}
|
|
84
|
+
logger.debug(`${type}: Successfully retrieved token from environment variable`);
|
|
63
85
|
return token;
|
|
64
86
|
};
|
|
65
87
|
case "azcli":
|
|
66
88
|
case "env":
|
|
67
89
|
if (type !== "env") {
|
|
90
|
+
logger.debug(`${type}: Setting AZURE_TOKEN_CREDENTIALS to 'dev' for development credential chain`);
|
|
68
91
|
process.env.AZURE_TOKEN_CREDENTIALS = "dev";
|
|
69
92
|
}
|
|
70
93
|
let credential = new DefaultAzureCredential(); // CodeQL [SM05138] resolved by explicitly setting AZURE_TOKEN_CREDENTIALS
|
|
@@ -76,11 +99,14 @@ function createAuthenticator(type, tenantId) {
|
|
|
76
99
|
return async () => {
|
|
77
100
|
const result = await credential.getToken(scopes);
|
|
78
101
|
if (!result) {
|
|
102
|
+
logger.error(`${type}: Failed to obtain token - credential.getToken returned null/undefined`);
|
|
79
103
|
throw new Error("Failed to obtain Azure DevOps token. Ensure you have Azure CLI logged or use interactive type of authentication.");
|
|
80
104
|
}
|
|
105
|
+
logger.debug(`${type}: Successfully obtained Azure DevOps token`);
|
|
81
106
|
return result.token;
|
|
82
107
|
};
|
|
83
108
|
default:
|
|
109
|
+
logger.debug(`Authenticator: Using OAuth interactive authentication (default)`);
|
|
84
110
|
const authenticator = new OAuthAuthenticator(tenantId);
|
|
85
111
|
return () => {
|
|
86
112
|
return authenticator.getToken();
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getBearerHandler, 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";
|
|
10
|
+
import { logger } from "./logger.js";
|
|
10
11
|
import { getOrgTenant } from "./org-tenants.js";
|
|
11
12
|
//import { configurePrompts } from "./prompts.js";
|
|
12
13
|
import { configureAllTools } from "./tools.js";
|
|
@@ -67,6 +68,16 @@ function getAzureDevOpsClient(getAzureDevOpsToken, userAgentComposer) {
|
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
async function main() {
|
|
71
|
+
logger.info("Starting Azure DevOps MCP Server", {
|
|
72
|
+
organization: orgName,
|
|
73
|
+
organizationUrl: orgUrl,
|
|
74
|
+
authentication: argv.authentication,
|
|
75
|
+
tenant: argv.tenant,
|
|
76
|
+
domains: argv.domains,
|
|
77
|
+
enabledDomains: Array.from(enabledDomains),
|
|
78
|
+
version: packageVersion,
|
|
79
|
+
isCodespace: isGitHubCodespaceEnv(),
|
|
80
|
+
});
|
|
70
81
|
const server = new McpServer({
|
|
71
82
|
name: "Azure DevOps MCP Server",
|
|
72
83
|
version: packageVersion,
|
|
@@ -89,6 +100,6 @@ async function main() {
|
|
|
89
100
|
await server.connect(transport);
|
|
90
101
|
}
|
|
91
102
|
main().catch((error) => {
|
|
92
|
-
|
|
103
|
+
logger.error("Fatal error in main():", error);
|
|
93
104
|
process.exit(1);
|
|
94
105
|
});
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import winston from "winston";
|
|
4
|
+
import { setLogLevel } from "@azure/logger";
|
|
5
|
+
const logLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
6
|
+
if (logLevel && ["verbose", "debug", "info", "warning", "error"].includes(logLevel)) {
|
|
7
|
+
// Map Winston log levels to Azure log levels
|
|
8
|
+
const logLevelMap = {
|
|
9
|
+
verbose: "verbose",
|
|
10
|
+
debug: "info",
|
|
11
|
+
info: "info",
|
|
12
|
+
warning: "warning",
|
|
13
|
+
error: "error",
|
|
14
|
+
};
|
|
15
|
+
const azureLogLevel = logLevelMap[logLevel];
|
|
16
|
+
setLogLevel(azureLogLevel);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Logger utility for MCP server
|
|
20
|
+
*
|
|
21
|
+
* Since MCP servers use stdio transport for communication on stdout,
|
|
22
|
+
* we log to stderr to avoid interfering with the MCP protocol.
|
|
23
|
+
*/
|
|
24
|
+
export const logger = winston.createLogger({
|
|
25
|
+
level: process.env.LOG_LEVEL || "info",
|
|
26
|
+
format: winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json()),
|
|
27
|
+
transports: [
|
|
28
|
+
new winston.transports.Stream({
|
|
29
|
+
stream: process.stderr,
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
// Prevent Winston from exiting on error
|
|
33
|
+
exitOnError: false,
|
|
34
|
+
});
|
package/dist/org-tenants.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { logger } from "./logger.js";
|
|
4
5
|
import { homedir } from "os";
|
|
5
6
|
import { join } from "path";
|
|
6
7
|
const CACHE_FILE = join(homedir(), ".ado_orgs.cache");
|
|
@@ -20,7 +21,7 @@ async function trySavingCache(cache) {
|
|
|
20
21
|
await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
|
|
21
22
|
}
|
|
22
23
|
catch (error) {
|
|
23
|
-
|
|
24
|
+
logger.error("Failed to save org tenants cache:", error);
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
async function fetchTenantFromApi(orgName) {
|
|
@@ -65,11 +66,11 @@ export async function getOrgTenant(orgName) {
|
|
|
65
66
|
catch (error) {
|
|
66
67
|
// If we have an expired cache entry, return it as fallback
|
|
67
68
|
if (cachedEntry) {
|
|
68
|
-
|
|
69
|
+
logger.error(`Failed to fetch fresh tenant for ADO org ${orgName}, using expired cache entry:`, error);
|
|
69
70
|
return cachedEntry.tenantId;
|
|
70
71
|
}
|
|
71
72
|
// No cache entry available, log and return empty result
|
|
72
|
-
|
|
73
|
+
logger.error(`Failed to fetch tenant for ADO org ${orgName}:`, error);
|
|
73
74
|
return undefined;
|
|
74
75
|
}
|
|
75
76
|
}
|
package/dist/shared/domains.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
+
import { logger } from "../logger.js";
|
|
3
4
|
/**
|
|
4
5
|
* Available Azure DevOps MCP domains
|
|
5
6
|
*/
|
|
@@ -68,7 +69,7 @@ export class DomainsManager {
|
|
|
68
69
|
this.enableAllDomains();
|
|
69
70
|
}
|
|
70
71
|
else {
|
|
71
|
-
|
|
72
|
+
logger.error(`Error: Specified invalid domain '${domain}'. Please specify exactly as available domains: ${Object.values(Domain).join(", ")}`);
|
|
72
73
|
}
|
|
73
74
|
});
|
|
74
75
|
if (this.enabledDomains.size === 0) {
|
package/dist/tools/pipelines.js
CHANGED
|
@@ -65,8 +65,8 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
65
65
|
}, async ({ project, name, folder, yamlPath, repositoryType, repositoryName, repositoryId, repositoryConnectionId }) => {
|
|
66
66
|
const connection = await connectionProvider();
|
|
67
67
|
const pipelinesApi = await connection.getPipelinesApi();
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
const repositoryTypeEnumValue = safeEnumConvert(RepositoryType, repositoryType);
|
|
69
|
+
const repositoryPayload = {
|
|
70
70
|
type: repositoryType,
|
|
71
71
|
};
|
|
72
72
|
if (repositoryTypeEnumValue === RepositoryType.AzureReposGit) {
|
|
@@ -94,6 +94,7 @@ function trimPullRequest(pr, includeDescription = false) {
|
|
|
94
94
|
uniqueName: pr.createdBy?.uniqueName,
|
|
95
95
|
},
|
|
96
96
|
creationDate: pr.creationDate,
|
|
97
|
+
closedDate: pr.closedDate,
|
|
97
98
|
title: pr.title,
|
|
98
99
|
...(includeDescription ? { description: pr.description ?? "" } : {}),
|
|
99
100
|
isDraft: pr.isDraft,
|
|
@@ -472,14 +473,38 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
472
473
|
project: z.string().optional().describe("Project ID or project name (optional)"),
|
|
473
474
|
iteration: z.number().optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
|
|
474
475
|
baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
|
|
475
|
-
top: z.number().default(100).describe("The maximum number of threads to return."),
|
|
476
|
-
skip: z.number().default(0).describe("The number of threads to skip."),
|
|
476
|
+
top: z.number().default(100).describe("The maximum number of threads to return after filtering."),
|
|
477
|
+
skip: z.number().default(0).describe("The number of threads to skip after filtering."),
|
|
477
478
|
fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of trimmed data."),
|
|
478
|
-
|
|
479
|
+
status: z
|
|
480
|
+
.enum(getEnumKeys(CommentThreadStatus))
|
|
481
|
+
.optional()
|
|
482
|
+
.describe("Filter threads by status. If not specified, returns threads of all statuses."),
|
|
483
|
+
authorEmail: z.string().optional().describe("Filter threads by the email of the thread author (first comment author)."),
|
|
484
|
+
authorDisplayName: z.string().optional().describe("Filter threads by the display name of the thread author (first comment author). Case-insensitive partial matching."),
|
|
485
|
+
}, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, top, skip, fullResponse, status, authorEmail, authorDisplayName }) => {
|
|
479
486
|
const connection = await connectionProvider();
|
|
480
487
|
const gitApi = await connection.getGitApi();
|
|
481
488
|
const threads = await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration);
|
|
482
|
-
|
|
489
|
+
let filteredThreads = threads;
|
|
490
|
+
if (status !== undefined) {
|
|
491
|
+
const statusValue = CommentThreadStatus[status];
|
|
492
|
+
filteredThreads = filteredThreads?.filter((thread) => thread.status === statusValue);
|
|
493
|
+
}
|
|
494
|
+
if (authorEmail !== undefined) {
|
|
495
|
+
filteredThreads = filteredThreads?.filter((thread) => {
|
|
496
|
+
const firstComment = thread.comments?.[0];
|
|
497
|
+
return firstComment?.author?.uniqueName?.toLowerCase() === authorEmail.toLowerCase();
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
if (authorDisplayName !== undefined) {
|
|
501
|
+
const lowerAuthorName = authorDisplayName.toLowerCase();
|
|
502
|
+
filteredThreads = filteredThreads?.filter((thread) => {
|
|
503
|
+
const firstComment = thread.comments?.[0];
|
|
504
|
+
return firstComment?.author?.displayName?.toLowerCase().includes(lowerAuthorName);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
const paginatedThreads = filteredThreads?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
|
|
483
508
|
if (fullResponse) {
|
|
484
509
|
return {
|
|
485
510
|
content: [{ type: "text", text: JSON.stringify(paginatedThreads, null, 2) }],
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.2.2-nightly.
|
|
1
|
+
export const packageVersion = "2.2.2-nightly.20251125";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-devops/mcp",
|
|
3
|
-
"version": "2.2.2-nightly.
|
|
3
|
+
"version": "2.2.2-nightly.20251125",
|
|
4
4
|
"description": "MCP server for interacting with Azure DevOps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Microsoft Corporation",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"azure-devops-extension-api": "^4.252.0",
|
|
44
44
|
"azure-devops-extension-sdk": "^4.0.2",
|
|
45
45
|
"azure-devops-node-api": "^15.1.0",
|
|
46
|
+
"winston": "^3.18.3",
|
|
46
47
|
"yargs": "^18.0.0",
|
|
47
48
|
"zod": "^3.25.63",
|
|
48
49
|
"zod-to-json-schema": "^3.24.5"
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@modelcontextprotocol/inspector": "^0.17.0",
|
|
52
53
|
"@types/jest": "^30.0.0",
|
|
53
|
-
"@types/node": "^22",
|
|
54
|
+
"@types/node": "^22.19.1",
|
|
54
55
|
"eslint-config-prettier": "10.1.8",
|
|
55
56
|
"eslint-plugin-header": "^3.1.1",
|
|
56
57
|
"glob": "^11.0.3",
|