@azure-devops/mcp 2.2.2-nightly.20251120 → 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/README.md +1 -107
- 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 +11 -3
package/README.md
CHANGED
|
@@ -42,113 +42,7 @@ The Azure DevOps MCP Server is built from tools that are concise, simple, focuse
|
|
|
42
42
|
|
|
43
43
|
## ⚙️ Supported Tools
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
### 🧿 Core
|
|
48
|
-
|
|
49
|
-
- **core_list_project_teams**: Retrieve a list of teams for the specified Azure DevOps project.
|
|
50
|
-
- **core_list_projects**: Retrieve a list of projects in your Azure DevOps organization.
|
|
51
|
-
- **core_get_identity_ids**: Retrieve Azure DevOps identity IDs for a list of unique names.
|
|
52
|
-
|
|
53
|
-
### ⚒️ Work
|
|
54
|
-
|
|
55
|
-
- **work_list_team_iterations**: Retrieve a list of iterations for a specific team in a project.
|
|
56
|
-
- **work_list_iterations**: List all iterations in a specified Azure DevOps project.
|
|
57
|
-
- **work_create_iterations**: Create new iterations in a specified Azure DevOps project.
|
|
58
|
-
- **work_assign_iterations**: Assign existing iterations to a specific team in a project.
|
|
59
|
-
- **work_get_team_capacity**: Get the team capacity of a specific team and iteration in a project.
|
|
60
|
-
- **work_update_team_capacity**: Update the team capacity of a team member for a specific iteration in a project.
|
|
61
|
-
- **work_get_iteration_capacities**: Get an iteration's capacity for all teams in iteration and project.
|
|
62
|
-
|
|
63
|
-
### 📅 Work Items
|
|
64
|
-
|
|
65
|
-
- **wit_my_work_items**: Retrieve a list of work items relevant to the authenticated user.
|
|
66
|
-
- **wit_list_backlogs**: Retrieve a list of backlogs for a given project and team.
|
|
67
|
-
- **wit_list_backlog_work_items**: Retrieve a list of backlogs for a given project, team, and backlog category.
|
|
68
|
-
- **wit_get_work_item**: Get a single work item by ID.
|
|
69
|
-
- **wit_get_work_items_batch_by_ids**: Retrieve a list of work items by IDs in batch.
|
|
70
|
-
- **wit_update_work_item**: Update a work item by ID with specified fields.
|
|
71
|
-
- **wit_create_work_item**: Create a new work item in a specified project and work item type.
|
|
72
|
-
- **wit_list_work_item_comments**: Retrieve a list of comments for a work item by ID.
|
|
73
|
-
- **wit_list_work_item_revisions**: Retrieve a list of revisions for a work item by ID.
|
|
74
|
-
- **wit_get_work_items_for_iteration**: Retrieve a list of work items for a specified iteration.
|
|
75
|
-
- **wit_add_work_item_comment**: Add a comment to a work item by ID.
|
|
76
|
-
- **wit_add_child_work_items**: Create one or more child work items of a specific work item type for the given parent ID.
|
|
77
|
-
- **wit_link_work_item_to_pull_request**: Link a single work item to an existing pull request.
|
|
78
|
-
- **wit_get_work_item_type**: Get a specific work item type.
|
|
79
|
-
- **wit_get_query**: Get a query by its ID or path.
|
|
80
|
-
- **wit_get_query_results_by_id**: Retrieve the results of a work item query given the query ID.
|
|
81
|
-
- **wit_update_work_items_batch**: Update work items in batch.
|
|
82
|
-
- **wit_work_items_link**: Link work items together in batch.
|
|
83
|
-
- **wit_work_item_unlink**: Unlink one or many links from a work item.
|
|
84
|
-
- **wit_add_artifact_link**: Link to artifacts like branch, pull request, commit, and build.
|
|
85
|
-
|
|
86
|
-
### 📁 Repositories
|
|
87
|
-
|
|
88
|
-
- **repo_list_repos_by_project**: Retrieve a list of repositories for a given project.
|
|
89
|
-
- **repo_list_pull_requests_by_repo_or_project**: Retrieve a list of pull requests for a given repository or project.
|
|
90
|
-
- **repo_list_branches_by_repo**: Retrieve a list of branches for a given repository.
|
|
91
|
-
- **repo_list_my_branches_by_repo**: Retrieve a list of your branches for a given repository ID.
|
|
92
|
-
- **repo_list_pull_requests_by_commits**: List pull requests associated with commits.
|
|
93
|
-
- **repo_list_pull_request_threads**: Retrieve a list of comment threads for a pull request.
|
|
94
|
-
- **repo_list_pull_request_thread_comments**: Retrieve a list of comments in a pull request thread.
|
|
95
|
-
- **repo_get_repo_by_name_or_id**: Get the repository by project and repository name or ID.
|
|
96
|
-
- **repo_get_branch_by_name**: Get a branch by its name.
|
|
97
|
-
- **repo_get_pull_request_by_id**: Get a pull request by its ID.
|
|
98
|
-
- **repo_create_pull_request**: Create a new pull request.
|
|
99
|
-
- **repo_create_branch**: Create a new branch in the repository.
|
|
100
|
-
- **repo_update_pull_request**: Update various fields of an existing pull request (title, description, draft status, target branch).
|
|
101
|
-
- **repo_update_pull_request_reviewers**: Add or remove reviewers for an existing pull request.
|
|
102
|
-
- **repo_reply_to_comment**: Replies to a specific comment on a pull request.
|
|
103
|
-
- **repo_resolve_comment**: Resolves a specific comment thread on a pull request.
|
|
104
|
-
- **repo_search_commits**: Searches for commits.
|
|
105
|
-
- **repo_create_pull_request_thread**: Creates a new comment thread on a pull request.
|
|
106
|
-
|
|
107
|
-
### 🚀 Pipelines
|
|
108
|
-
|
|
109
|
-
- **pipelines_get_build_definitions**: Retrieve a list of build definitions for a given project.
|
|
110
|
-
- **pipelines_get_build_definition_revisions**: Retrieve a list of revisions for a specific build definition.
|
|
111
|
-
- **pipelines_get_builds**: Retrieve a list of builds for a given project.
|
|
112
|
-
- **pipelines_get_build_log**: Retrieve the logs for a specific build.
|
|
113
|
-
- **pipelines_get_build_log_by_id**: Get a specific build log by log ID.
|
|
114
|
-
- **pipelines_get_build_changes**: Get the changes associated with a specific build.
|
|
115
|
-
- **pipelines_get_build_status**: Fetch the status of a specific build.
|
|
116
|
-
- **pipelines_update_build_stage**: Update the stage of a specific build.
|
|
117
|
-
- **pipelines_create_pipeline**: Creates a pipeline definition with YAML configuration for a given project.
|
|
118
|
-
- **pipelines_get_run**: Gets a run for a particular pipeline.
|
|
119
|
-
- **pipelines_list_runs**: Gets top 10000 runs for a particular pipeline.
|
|
120
|
-
- **pipelines_run_pipeline**: Starts a new run of a pipeline.
|
|
121
|
-
|
|
122
|
-
### Advanced Security
|
|
123
|
-
|
|
124
|
-
- **advsec_get_alerts**: Retrieve Advanced Security alerts for a repository.
|
|
125
|
-
- **advsec_get_alert_details**: Get detailed information about a specific Advanced Security alert.
|
|
126
|
-
|
|
127
|
-
### 🧪 Test Plans
|
|
128
|
-
|
|
129
|
-
- **testplan_create_test_plan**: Create a new test plan in the project.
|
|
130
|
-
- **testplan_create_test_case**: Create a new test case work item.
|
|
131
|
-
- **testplan_update_test_case_steps**: Update an existing test case work item's steps.
|
|
132
|
-
- **testplan_add_test_cases_to_suite**: Add existing test cases to a test suite.
|
|
133
|
-
- **testplan_list_test_plans**: Retrieve a paginated list of test plans from an Azure DevOps project. Allows filtering for active plans and toggling detailed information.
|
|
134
|
-
- **testplan_list_test_cases**: Get a list of test cases in the test plan.
|
|
135
|
-
- **testplan_show_test_results_from_build_id**: Get a list of test results for a given project and build ID.
|
|
136
|
-
- **testplan_create_test_suite**: Creates a new test suite in a test plan.
|
|
137
|
-
|
|
138
|
-
### 📖 Wiki
|
|
139
|
-
|
|
140
|
-
- **wiki_list_wikis**: Retrieve a list of wikis for an organization or project.
|
|
141
|
-
- **wiki_get_wiki**: Get the wiki by wikiIdentifier.
|
|
142
|
-
- **wiki_list_pages**: Retrieve a list of wiki pages for a specific wiki and project.
|
|
143
|
-
- **wiki_get_page**: Retrieve wiki page metadata by path.
|
|
144
|
-
- **wiki_get_page_content**: Retrieve wiki page content by wikiIdentifier and path.
|
|
145
|
-
- **wiki_create_or_update_page**: Create or update wiki pages with full content support.
|
|
146
|
-
|
|
147
|
-
### 🔎 Search
|
|
148
|
-
|
|
149
|
-
- **search_code**: Get code search results for a given search text.
|
|
150
|
-
- **search_wiki**: Get wiki search results for a given search text.
|
|
151
|
-
- **search_workitem**: Get work item search results for a given search text.
|
|
45
|
+
See [TOOLSET.md](./docs/TOOLSET.md) for a comprehensive list.
|
|
152
46
|
|
|
153
47
|
## 🔌 Installation & Getting Started
|
|
154
48
|
|
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",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"prebuild": "node -p \"'export const packageVersion = ' + JSON.stringify(require('./package.json').version) + ';\\n'\" > src/version.ts && prettier --write src/version.ts",
|
|
26
26
|
"validate-tools": "tsc --noEmit && node scripts/build-validate-tools.js",
|
|
27
27
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
28
|
-
"prepare": "npm run build",
|
|
28
|
+
"prepare": "npm run build && husky",
|
|
29
29
|
"watch": "tsc --watch",
|
|
30
30
|
"inspect": "ALLOWED_ORIGINS=http://127.0.0.1:6274 npx @modelcontextprotocol/inspector node dist/index.js",
|
|
31
31
|
"start": "node -r tsconfig-paths/register dist/index.js",
|
|
@@ -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,17 +51,24 @@
|
|
|
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",
|
|
58
|
+
"husky": "^9.1.7",
|
|
57
59
|
"jest": "^30.0.2",
|
|
58
60
|
"jest-extended": "^7.0.0",
|
|
61
|
+
"lint-staged": "^16.2.7",
|
|
59
62
|
"prettier": "3.6.2",
|
|
60
63
|
"shx": "^0.4.0",
|
|
61
64
|
"ts-jest": "^29.4.0",
|
|
62
65
|
"tsconfig-paths": "^4.2.0",
|
|
63
66
|
"typescript": "^5.9.3",
|
|
64
67
|
"typescript-eslint": "^8.46.4"
|
|
68
|
+
},
|
|
69
|
+
"lint-staged": {
|
|
70
|
+
"**/*.(js|ts|jsx|tsx|json|css|md)": [
|
|
71
|
+
"npm run format"
|
|
72
|
+
]
|
|
65
73
|
}
|
|
66
74
|
}
|