@azure-devops/mcp 2.2.2 → 2.3.0-nightly.20251204

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 CHANGED
@@ -42,111 +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
- Interact with these Azure DevOps services:
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_get_work_items_for_iteration**: Retrieve a list of work items for a specified iteration.
74
- - **wit_add_work_item_comment**: Add a comment to a work item by ID.
75
- - **wit_add_child_work_items**: Create one or more child work items of a specific work item type for the given parent ID.
76
- - **wit_link_work_item_to_pull_request**: Link a single work item to an existing pull request.
77
- - **wit_get_work_item_type**: Get a specific work item type.
78
- - **wit_get_query**: Get a query by its ID or path.
79
- - **wit_get_query_results_by_id**: Retrieve the results of a work item query given the query ID.
80
- - **wit_update_work_items_batch**: Update work items in batch.
81
- - **wit_work_items_link**: Link work items together in batch.
82
- - **wit_work_item_unlink**: Unlink one or many links from a work item.
83
- - **wit_add_artifact_link**: Link to artifacts like branch, pull request, commit, and build.
84
-
85
- ### 📁 Repositories
86
-
87
- - **repo_list_repos_by_project**: Retrieve a list of repositories for a given project.
88
- - **repo_list_pull_requests_by_repo_or_project**: Retrieve a list of pull requests for a given repository or project.
89
- - **repo_list_branches_by_repo**: Retrieve a list of branches for a given repository.
90
- - **repo_list_my_branches_by_repo**: Retrieve a list of your branches for a given repository ID.
91
- - **repo_list_pull_requests_by_commits**: List pull requests associated with commits.
92
- - **repo_list_pull_request_threads**: Retrieve a list of comment threads for a pull request.
93
- - **repo_list_pull_request_thread_comments**: Retrieve a list of comments in a pull request thread.
94
- - **repo_get_repo_by_name_or_id**: Get the repository by project and repository name or ID.
95
- - **repo_get_branch_by_name**: Get a branch by its name.
96
- - **repo_get_pull_request_by_id**: Get a pull request by its ID.
97
- - **repo_create_pull_request**: Create a new pull request.
98
- - **repo_create_branch**: Create a new branch in the repository.
99
- - **repo_update_pull_request**: Update various fields of an existing pull request (title, description, draft status, target branch).
100
- - **repo_update_pull_request_reviewers**: Add or remove reviewers for an existing pull request.
101
- - **repo_reply_to_comment**: Replies to a specific comment on a pull request.
102
- - **repo_resolve_comment**: Resolves a specific comment thread on a pull request.
103
- - **repo_search_commits**: Searches for commits.
104
- - **repo_create_pull_request_thread**: Creates a new comment thread on a pull request.
105
-
106
- ### 🚀 Pipelines
107
-
108
- - **pipelines_get_build_definitions**: Retrieve a list of build definitions for a given project.
109
- - **pipelines_get_build_definition_revisions**: Retrieve a list of revisions for a specific build definition.
110
- - **pipelines_get_builds**: Retrieve a list of builds for a given project.
111
- - **pipelines_get_build_log**: Retrieve the logs for a specific build.
112
- - **pipelines_get_build_log_by_id**: Get a specific build log by log ID.
113
- - **pipelines_get_build_changes**: Get the changes associated with a specific build.
114
- - **pipelines_get_build_status**: Fetch the status of a specific build.
115
- - **pipelines_update_build_stage**: Update the stage of a specific build.
116
- - **pipelines_get_run**: Gets a run for a particular pipeline.
117
- - **pipelines_list_runs**: Gets top 10000 runs for a particular pipeline.
118
- - **pipelines_run_pipeline**: Starts a new run of a pipeline.
119
-
120
- ### Advanced Security
121
-
122
- - **advsec_get_alerts**: Retrieve Advanced Security alerts for a repository.
123
- - **advsec_get_alert_details**: Get detailed information about a specific Advanced Security alert.
124
-
125
- ### 🧪 Test Plans
126
-
127
- - **testplan_create_test_plan**: Create a new test plan in the project.
128
- - **testplan_create_test_case**: Create a new test case work item.
129
- - **testplan_update_test_case_steps**: Update an existing test case work item's steps.
130
- - **testplan_add_test_cases_to_suite**: Add existing test cases to a test suite.
131
- - **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.
132
- - **testplan_list_test_cases**: Get a list of test cases in the test plan.
133
- - **testplan_show_test_results_from_build_id**: Get a list of test results for a given project and build ID.
134
- - **testplan_create_test_suite**: Creates a new test suite in a test plan.
135
-
136
- ### 📖 Wiki
137
-
138
- - **wiki_list_wikis**: Retrieve a list of wikis for an organization or project.
139
- - **wiki_get_wiki**: Get the wiki by wikiIdentifier.
140
- - **wiki_list_pages**: Retrieve a list of wiki pages for a specific wiki and project.
141
- - **wiki_get_page**: Retrieve wiki page metadata by path.
142
- - **wiki_get_page_content**: Retrieve wiki page content by wikiIdentifier and path.
143
- - **wiki_create_or_update_page**: Create or update wiki pages with full content support.
144
-
145
- ### 🔎 Search
146
-
147
- - **search_code**: Get code search results for a given search text.
148
- - **search_wiki**: Get wiki search results for a given search text.
149
- - **search_workitem**: Get work item search results for a given search text.
45
+ See [TOOLSET.md](./docs/TOOLSET.md) for a comprehensive list.
150
46
 
151
47
  ## 🔌 Installation & Getting Started
152
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
- console.error("Fatal error in main():", error);
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
+ });
@@ -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
- console.error("Failed to save org tenants cache:", error);
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
- console.error(`Failed to fetch fresh tenant for ADO org ${orgName}, using expired cache entry:`, error);
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
- console.error(`Failed to fetch tenant for ADO org ${orgName}:`, error);
73
+ logger.error(`Failed to fetch tenant for ADO org ${orgName}:`, error);
73
74
  return undefined;
74
75
  }
75
76
  }
package/dist/prompts.js CHANGED
File without changes
@@ -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
- console.error(`Error: Specified invalid domain '${domain}'. Please specify exactly as available domains: ${Object.values(Domain).join(", ")}`);
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) {
@@ -4,6 +4,7 @@ import { apiVersion, getEnumKeys, safeEnumConvert } from "../utils.js";
4
4
  import { BuildQueryOrder, DefinitionQueryOrder } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
5
5
  import { z } from "zod";
6
6
  import { StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
7
+ import { ConfigurationType, RepositoryType } from "azure-devops-node-api/interfaces/PipelinesInterfaces.js";
7
8
  const PIPELINE_TOOLS = {
8
9
  pipelines_get_builds: "pipelines_get_builds",
9
10
  pipelines_get_build_changes: "pipelines_get_build_changes",
@@ -13,6 +14,7 @@ const PIPELINE_TOOLS = {
13
14
  pipelines_get_build_log_by_id: "pipelines_get_build_log_by_id",
14
15
  pipelines_get_build_status: "pipelines_get_build_status",
15
16
  pipelines_update_build_stage: "pipelines_update_build_stage",
17
+ pipelines_create_pipeline: "pipelines_create_pipeline",
16
18
  pipelines_get_run: "pipelines_get_run",
17
19
  pipelines_list_runs: "pipelines_list_runs",
18
20
  pipelines_run_pipeline: "pipelines_run_pipeline",
@@ -47,6 +49,56 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
47
49
  content: [{ type: "text", text: JSON.stringify(buildDefinitions, null, 2) }],
48
50
  };
49
51
  });
52
+ const variableSchema = z.object({
53
+ value: z.string().optional(),
54
+ isSecret: z.boolean().optional(),
55
+ });
56
+ server.tool(PIPELINE_TOOLS.pipelines_create_pipeline, "Creates a pipeline definition with YAML configuration for a given project.", {
57
+ project: z.string().describe("Project ID or name to run the build in."),
58
+ name: z.string().describe("Name of the new pipeline."),
59
+ folder: z.string().optional().describe("Folder path for the new pipeline. Defaults to '\\' if not specified."),
60
+ yamlPath: z.string().describe("The path to the pipeline's YAML file in the repository"),
61
+ repositoryType: z.enum(getEnumKeys(RepositoryType)).describe("The type of repository where the pipeline's YAML file is located."),
62
+ repositoryName: z.string().describe("The name of the repository. In case of GitHub repository, this is the full name (:owner/:repo) - e.g. octocat/Hello-World."),
63
+ repositoryId: z.string().optional().describe("The ID of the repository."),
64
+ repositoryConnectionId: z.string().optional().describe("The service connection ID for GitHub repositories. Not required for Azure Repos Git."),
65
+ }, async ({ project, name, folder, yamlPath, repositoryType, repositoryName, repositoryId, repositoryConnectionId }) => {
66
+ const connection = await connectionProvider();
67
+ const pipelinesApi = await connection.getPipelinesApi();
68
+ const repositoryTypeEnumValue = safeEnumConvert(RepositoryType, repositoryType);
69
+ const repositoryPayload = {
70
+ type: repositoryType,
71
+ };
72
+ if (repositoryTypeEnumValue === RepositoryType.AzureReposGit) {
73
+ repositoryPayload.id = repositoryId;
74
+ repositoryPayload.name = repositoryName;
75
+ }
76
+ else if (repositoryTypeEnumValue === RepositoryType.GitHub) {
77
+ if (!repositoryConnectionId) {
78
+ throw new Error("Parameter 'repositoryConnectionId' is required for GitHub repositories.");
79
+ }
80
+ repositoryPayload.connection = { id: repositoryConnectionId };
81
+ repositoryPayload.fullname = repositoryName;
82
+ }
83
+ else {
84
+ throw new Error("Unsupported repository type");
85
+ }
86
+ const yamlConfigurationType = getEnumKeys(ConfigurationType).find((k) => ConfigurationType[k] === ConfigurationType.Yaml);
87
+ const createPipelineParams = {
88
+ name: name,
89
+ folder: folder || "\\",
90
+ configuration: {
91
+ type: yamlConfigurationType,
92
+ path: yamlPath,
93
+ repository: repositoryPayload,
94
+ variables: undefined,
95
+ },
96
+ };
97
+ const newPipeline = await pipelinesApi.createPipeline(createPipelineParams, project);
98
+ return {
99
+ content: [{ type: "text", text: JSON.stringify(newPipeline, null, 2) }],
100
+ };
101
+ });
50
102
  server.tool(PIPELINE_TOOLS.pipelines_get_build_definition_revisions, "Retrieves a list of revisions for a specific build definition.", {
51
103
  project: z.string().describe("Project ID or name to get the build definition revisions for"),
52
104
  definitionId: z.number().describe("ID of the build definition to get revisions for"),
@@ -154,10 +206,6 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
154
206
  content: [{ type: "text", text: JSON.stringify(pipelineRuns, null, 2) }],
155
207
  };
156
208
  });
157
- const variableSchema = z.object({
158
- value: z.string().optional(),
159
- isSecret: z.boolean().optional(),
160
- });
161
209
  const resourcesSchema = z.object({
162
210
  builds: z
163
211
  .record(z.string().describe("Name of the build resource."), z.object({