@azure-devops/mcp 2.2.2-nightly.20251121 → 2.2.2-nightly.20251126
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 +60 -5
- package/dist/tools/test-plans.js +256 -123
- 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) }],
|
|
@@ -584,10 +609,40 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
584
609
|
repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
|
|
585
610
|
pullRequestId: z.number().describe("The ID of the pull request to retrieve."),
|
|
586
611
|
includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to reference work items associated with the pull request."),
|
|
587
|
-
|
|
612
|
+
includeLabels: z.boolean().optional().default(false).describe("Whether to include a summary of labels in the response."),
|
|
613
|
+
}, async ({ repositoryId, pullRequestId, includeWorkItemRefs, includeLabels }) => {
|
|
588
614
|
const connection = await connectionProvider();
|
|
589
615
|
const gitApi = await connection.getGitApi();
|
|
590
616
|
const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, undefined, undefined, undefined, undefined, undefined, includeWorkItemRefs);
|
|
617
|
+
if (includeLabels) {
|
|
618
|
+
try {
|
|
619
|
+
const projectId = pullRequest.repository?.project?.id;
|
|
620
|
+
const projectName = pullRequest.repository?.project?.name;
|
|
621
|
+
const labels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId, projectName, projectId);
|
|
622
|
+
const labelNames = labels.map((label) => label.name).filter((name) => name !== undefined);
|
|
623
|
+
const enhancedResponse = {
|
|
624
|
+
...pullRequest,
|
|
625
|
+
labelSummary: {
|
|
626
|
+
labels: labelNames,
|
|
627
|
+
labelCount: labelNames.length,
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
return {
|
|
631
|
+
content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
console.warn(`Error fetching PR labels: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
636
|
+
// Fall back to the original response without labels
|
|
637
|
+
const enhancedResponse = {
|
|
638
|
+
...pullRequest,
|
|
639
|
+
labelSummary: {},
|
|
640
|
+
};
|
|
641
|
+
return {
|
|
642
|
+
content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
591
646
|
return {
|
|
592
647
|
content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
|
|
593
648
|
};
|
package/dist/tools/test-plans.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
+
import { SuiteExpand } from "azure-devops-node-api/interfaces/TestPlanInterfaces.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
const Test_Plan_Tools = {
|
|
5
6
|
create_test_plan: "testplan_create_test_plan",
|
|
@@ -9,6 +10,7 @@ const Test_Plan_Tools = {
|
|
|
9
10
|
test_results_from_build_id: "testplan_show_test_results_from_build_id",
|
|
10
11
|
list_test_cases: "testplan_list_test_cases",
|
|
11
12
|
list_test_plans: "testplan_list_test_plans",
|
|
13
|
+
list_test_suites: "testplan_list_test_suites",
|
|
12
14
|
create_test_suite: "testplan_create_test_suite",
|
|
13
15
|
};
|
|
14
16
|
function configureTestPlanTools(server, _, connectionProvider) {
|
|
@@ -18,13 +20,22 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
18
20
|
includePlanDetails: z.boolean().default(false).describe("Include detailed information about each test plan."),
|
|
19
21
|
continuationToken: z.string().optional().describe("Token to continue fetching test plans from a previous request."),
|
|
20
22
|
}, async ({ project, filterActivePlans, includePlanDetails, continuationToken }) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
try {
|
|
24
|
+
const owner = ""; //making owner an empty string untill we can figure out how to get owner id
|
|
25
|
+
const connection = await connectionProvider();
|
|
26
|
+
const testPlanApi = await connection.getTestPlanApi();
|
|
27
|
+
const testPlans = await testPlanApi.getTestPlans(project, owner, continuationToken, includePlanDetails, filterActivePlans);
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: JSON.stringify(testPlans, null, 2) }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: `Error listing test plans: ${errorMessage}` }],
|
|
36
|
+
isError: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
28
39
|
});
|
|
29
40
|
server.tool(Test_Plan_Tools.create_test_plan, "Creates a new test plan in the project.", {
|
|
30
41
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project where the test plan will be created."),
|
|
@@ -35,20 +46,29 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
35
46
|
endDate: z.string().optional().describe("The end date of the test plan"),
|
|
36
47
|
areaPath: z.string().optional().describe("The area path for the test plan"),
|
|
37
48
|
}, async ({ project, name, iteration, description, startDate, endDate, areaPath }) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
try {
|
|
50
|
+
const connection = await connectionProvider();
|
|
51
|
+
const testPlanApi = await connection.getTestPlanApi();
|
|
52
|
+
const testPlanToCreate = {
|
|
53
|
+
name,
|
|
54
|
+
iteration,
|
|
55
|
+
description,
|
|
56
|
+
startDate: startDate ? new Date(startDate) : undefined,
|
|
57
|
+
endDate: endDate ? new Date(endDate) : undefined,
|
|
58
|
+
areaPath,
|
|
59
|
+
};
|
|
60
|
+
const createdTestPlan = await testPlanApi.createTestPlan(testPlanToCreate, project);
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text", text: JSON.stringify(createdTestPlan, null, 2) }],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: `Error creating test plan: ${errorMessage}` }],
|
|
69
|
+
isError: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
52
72
|
});
|
|
53
73
|
server.tool(Test_Plan_Tools.create_test_suite, "Creates a new test suite in a test plan.", {
|
|
54
74
|
project: z.string().describe("Project ID or project name"),
|
|
@@ -56,20 +76,29 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
56
76
|
parentSuiteId: z.number().describe("ID of the parent suite under which the new suite will be created, if not given by user this can be id of a root suite of the test plan"),
|
|
57
77
|
name: z.string().describe("Name of the child test suite"),
|
|
58
78
|
}, async ({ project, planId, parentSuiteId, name }) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
try {
|
|
80
|
+
const connection = await connectionProvider();
|
|
81
|
+
const testPlanApi = await connection.getTestPlanApi();
|
|
82
|
+
const testSuiteToCreate = {
|
|
83
|
+
name,
|
|
84
|
+
parentSuite: {
|
|
85
|
+
id: parentSuiteId,
|
|
86
|
+
name: "",
|
|
87
|
+
},
|
|
88
|
+
suiteType: 2,
|
|
89
|
+
};
|
|
90
|
+
const createdTestSuite = await testPlanApi.createTestSuite(testSuiteToCreate, project, planId);
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: "text", text: JSON.stringify(createdTestSuite, null, 2) }],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: "text", text: `Error creating test suite: ${errorMessage}` }],
|
|
99
|
+
isError: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
73
102
|
});
|
|
74
103
|
server.tool(Test_Plan_Tools.add_test_cases_to_suite, "Adds existing test cases to a test suite.", {
|
|
75
104
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
@@ -77,14 +106,23 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
77
106
|
suiteId: z.number().describe("The ID of the test suite."),
|
|
78
107
|
testCaseIds: z.string().or(z.array(z.string())).describe("The ID(s) of the test case(s) to add. "),
|
|
79
108
|
}, async ({ project, planId, suiteId, testCaseIds }) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
109
|
+
try {
|
|
110
|
+
const connection = await connectionProvider();
|
|
111
|
+
const testApi = await connection.getTestApi();
|
|
112
|
+
// If testCaseIds is an array, convert it to comma-separated string
|
|
113
|
+
const testCaseIdsString = Array.isArray(testCaseIds) ? testCaseIds.join(",") : testCaseIds;
|
|
114
|
+
const addedTestCases = await testApi.addTestCasesToSuite(project, planId, suiteId, testCaseIdsString);
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: JSON.stringify(addedTestCases, null, 2) }],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: "text", text: `Error adding test cases to suite: ${errorMessage}` }],
|
|
123
|
+
isError: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
88
126
|
});
|
|
89
127
|
server.tool(Test_Plan_Tools.create_test_case, "Creates a new test case work item.", {
|
|
90
128
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
@@ -98,61 +136,70 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
98
136
|
iterationPath: z.string().optional().describe("The iteration path for the test case."),
|
|
99
137
|
testsWorkItemId: z.number().optional().describe("Optional work item id that will be set as a Microsoft.VSTS.Common.TestedBy-Reverse link to the test case."),
|
|
100
138
|
}, async ({ project, title, steps, priority, areaPath, iterationPath, testsWorkItemId }) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
op: "add",
|
|
111
|
-
path: "/fields/System.Title",
|
|
112
|
-
value: title,
|
|
113
|
-
});
|
|
114
|
-
if (testsWorkItemId) {
|
|
139
|
+
try {
|
|
140
|
+
const connection = await connectionProvider();
|
|
141
|
+
const witClient = await connection.getWorkItemTrackingApi();
|
|
142
|
+
let stepsXml;
|
|
143
|
+
if (steps) {
|
|
144
|
+
stepsXml = convertStepsToXml(steps);
|
|
145
|
+
}
|
|
146
|
+
// Create JSON patch document for work item
|
|
147
|
+
const patchDocument = [];
|
|
115
148
|
patchDocument.push({
|
|
116
149
|
op: "add",
|
|
117
|
-
path: "/
|
|
118
|
-
value:
|
|
119
|
-
rel: "Microsoft.VSTS.Common.TestedBy-Reverse",
|
|
120
|
-
url: `${connection.serverUrl}/${project}/_apis/wit/workItems/${testsWorkItemId}`,
|
|
121
|
-
},
|
|
150
|
+
path: "/fields/System.Title",
|
|
151
|
+
value: title,
|
|
122
152
|
});
|
|
153
|
+
if (testsWorkItemId) {
|
|
154
|
+
patchDocument.push({
|
|
155
|
+
op: "add",
|
|
156
|
+
path: "/relations/-",
|
|
157
|
+
value: {
|
|
158
|
+
rel: "Microsoft.VSTS.Common.TestedBy-Reverse",
|
|
159
|
+
url: `${connection.serverUrl}/${project}/_apis/wit/workItems/${testsWorkItemId}`,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (stepsXml) {
|
|
164
|
+
patchDocument.push({
|
|
165
|
+
op: "add",
|
|
166
|
+
path: "/fields/Microsoft.VSTS.TCM.Steps",
|
|
167
|
+
value: stepsXml,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (priority) {
|
|
171
|
+
patchDocument.push({
|
|
172
|
+
op: "add",
|
|
173
|
+
path: "/fields/Microsoft.VSTS.Common.Priority",
|
|
174
|
+
value: priority,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (areaPath) {
|
|
178
|
+
patchDocument.push({
|
|
179
|
+
op: "add",
|
|
180
|
+
path: "/fields/System.AreaPath",
|
|
181
|
+
value: areaPath,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (iterationPath) {
|
|
185
|
+
patchDocument.push({
|
|
186
|
+
op: "add",
|
|
187
|
+
path: "/fields/System.IterationPath",
|
|
188
|
+
value: iterationPath,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const workItem = await witClient.createWorkItem({}, patchDocument, project, "Test Case");
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
|
|
194
|
+
};
|
|
123
195
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (priority) {
|
|
132
|
-
patchDocument.push({
|
|
133
|
-
op: "add",
|
|
134
|
-
path: "/fields/Microsoft.VSTS.Common.Priority",
|
|
135
|
-
value: priority,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
if (areaPath) {
|
|
139
|
-
patchDocument.push({
|
|
140
|
-
op: "add",
|
|
141
|
-
path: "/fields/System.AreaPath",
|
|
142
|
-
value: areaPath,
|
|
143
|
-
});
|
|
196
|
+
catch (error) {
|
|
197
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
198
|
+
return {
|
|
199
|
+
content: [{ type: "text", text: `Error creating test case: ${errorMessage}` }],
|
|
200
|
+
isError: true,
|
|
201
|
+
};
|
|
144
202
|
}
|
|
145
|
-
if (iterationPath) {
|
|
146
|
-
patchDocument.push({
|
|
147
|
-
op: "add",
|
|
148
|
-
path: "/fields/System.IterationPath",
|
|
149
|
-
value: iterationPath,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
const workItem = await witClient.createWorkItem({}, patchDocument, project, "Test Case");
|
|
153
|
-
return {
|
|
154
|
-
content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
|
|
155
|
-
};
|
|
156
203
|
});
|
|
157
204
|
server.tool(Test_Plan_Tools.update_test_case_steps, "Update an existing test case work item.", {
|
|
158
205
|
id: z.number().describe("The ID of the test case work item to update."),
|
|
@@ -160,48 +207,134 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
160
207
|
.string()
|
|
161
208
|
.describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one|Expected result one\n2. Step two|Expected result two. USE '|' as the delimiter between step and expected result. DO NOT use '|' in the description of the step or expected result."),
|
|
162
209
|
}, async ({ id, steps }) => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
210
|
+
try {
|
|
211
|
+
const connection = await connectionProvider();
|
|
212
|
+
const witClient = await connection.getWorkItemTrackingApi();
|
|
213
|
+
let stepsXml;
|
|
214
|
+
if (steps) {
|
|
215
|
+
stepsXml = convertStepsToXml(steps);
|
|
216
|
+
}
|
|
217
|
+
// Create JSON patch document for work item
|
|
218
|
+
const patchDocument = [];
|
|
219
|
+
if (stepsXml) {
|
|
220
|
+
patchDocument.push({
|
|
221
|
+
op: "add",
|
|
222
|
+
path: "/fields/Microsoft.VSTS.TCM.Steps",
|
|
223
|
+
value: stepsXml,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const workItem = await witClient.updateWorkItem({}, patchDocument, id);
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: "text", text: `Error updating test case steps: ${errorMessage}` }],
|
|
235
|
+
isError: true,
|
|
236
|
+
};
|
|
177
237
|
}
|
|
178
|
-
const workItem = await witClient.updateWorkItem({}, patchDocument, id);
|
|
179
|
-
return {
|
|
180
|
-
content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
|
|
181
|
-
};
|
|
182
238
|
});
|
|
183
239
|
server.tool(Test_Plan_Tools.list_test_cases, "Gets a list of test cases in the test plan.", {
|
|
184
240
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
185
241
|
planid: z.number().describe("The ID of the test plan."),
|
|
186
242
|
suiteid: z.number().describe("The ID of the test suite."),
|
|
187
243
|
}, async ({ project, planid, suiteid }) => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
244
|
+
try {
|
|
245
|
+
const connection = await connectionProvider();
|
|
246
|
+
const coreApi = await connection.getTestPlanApi();
|
|
247
|
+
const testcases = await coreApi.getTestCaseList(project, planid, suiteid);
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: "text", text: JSON.stringify(testcases, null, 2) }],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
254
|
+
return {
|
|
255
|
+
content: [{ type: "text", text: `Error listing test cases: ${errorMessage}` }],
|
|
256
|
+
isError: true,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
194
259
|
});
|
|
195
260
|
server.tool(Test_Plan_Tools.test_results_from_build_id, "Gets a list of test results for a given project and build ID.", {
|
|
196
261
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
197
262
|
buildid: z.number().describe("The ID of the build."),
|
|
198
263
|
}, async ({ project, buildid }) => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
264
|
+
try {
|
|
265
|
+
const connection = await connectionProvider();
|
|
266
|
+
const coreApi = await connection.getTestResultsApi();
|
|
267
|
+
const testResults = await coreApi.getTestResultDetailsForBuild(project, buildid);
|
|
268
|
+
return {
|
|
269
|
+
content: [{ type: "text", text: JSON.stringify(testResults, null, 2) }],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
274
|
+
return {
|
|
275
|
+
content: [{ type: "text", text: `Error fetching test results: ${errorMessage}` }],
|
|
276
|
+
isError: true,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
server.tool(Test_Plan_Tools.list_test_suites, "Retrieve a paginated list of test suites from an Azure DevOps project and Test Plan Id.", {
|
|
281
|
+
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
282
|
+
planId: z.number().describe("The ID of the test plan."),
|
|
283
|
+
continuationToken: z.string().optional().describe("Token to continue fetching test plans from a previous request."),
|
|
284
|
+
}, async ({ project, planId, continuationToken }) => {
|
|
285
|
+
try {
|
|
286
|
+
const connection = await connectionProvider();
|
|
287
|
+
const testPlanApi = await connection.getTestPlanApi();
|
|
288
|
+
const expand = SuiteExpand.Children;
|
|
289
|
+
const testSuites = await testPlanApi.getTestSuitesForPlan(project, planId, expand, continuationToken);
|
|
290
|
+
// The API returns a flat list where the root suite is first, followed by all nested suites
|
|
291
|
+
// We need to build a proper hierarchy by creating a map and assembling the tree
|
|
292
|
+
// Create a map of all suites by ID for quick lookup
|
|
293
|
+
const suiteMap = new Map();
|
|
294
|
+
testSuites.forEach((suite) => {
|
|
295
|
+
suiteMap.set(suite.id, {
|
|
296
|
+
id: suite.id,
|
|
297
|
+
name: suite.name,
|
|
298
|
+
parentSuiteId: suite.parentSuite?.id,
|
|
299
|
+
children: [],
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
// Build the hierarchy by linking children to parents
|
|
303
|
+
const roots = [];
|
|
304
|
+
suiteMap.forEach((suite) => {
|
|
305
|
+
if (suite.parentSuiteId && suiteMap.has(suite.parentSuiteId)) {
|
|
306
|
+
// This is a child suite, add it to its parent's children array
|
|
307
|
+
const parent = suiteMap.get(suite.parentSuiteId);
|
|
308
|
+
parent.children.push(suite);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// This is a root suite (no parent or parent not in map)
|
|
312
|
+
roots.push(suite);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
// Clean up the output - remove parentSuiteId and empty children arrays
|
|
316
|
+
const cleanSuite = (suite) => {
|
|
317
|
+
const cleaned = {
|
|
318
|
+
id: suite.id,
|
|
319
|
+
name: suite.name,
|
|
320
|
+
};
|
|
321
|
+
if (suite.children && suite.children.length > 0) {
|
|
322
|
+
cleaned.children = suite.children.map((child) => cleanSuite(child));
|
|
323
|
+
}
|
|
324
|
+
return cleaned;
|
|
325
|
+
};
|
|
326
|
+
const result = roots.map((root) => cleanSuite(root));
|
|
327
|
+
return {
|
|
328
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: "text", text: `Error listing test suites: ${errorMessage}` }],
|
|
335
|
+
isError: true,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
205
338
|
});
|
|
206
339
|
}
|
|
207
340
|
/*
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.2.2-nightly.
|
|
1
|
+
export const packageVersion = "2.2.2-nightly.20251126";
|
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.20251126",
|
|
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",
|