@azure-devops/mcp 1.0.0 → 1.1.0

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/index.js CHANGED
@@ -7,7 +7,7 @@ import * as azdev from "azure-devops-node-api";
7
7
  import { DefaultAzureCredential } from "@azure/identity";
8
8
  import { configurePrompts } from "./prompts.js";
9
9
  import { configureAllTools } from "./tools.js";
10
- import { userAgent } from "./utils.js";
10
+ import { UserAgentComposer } from "./useragent.js";
11
11
  import { packageVersion } from "./version.js";
12
12
  const args = process.argv.slice(2);
13
13
  if (args.length === 0) {
@@ -22,25 +22,30 @@ async function getAzureDevOpsToken() {
22
22
  const token = await credential.getToken("499b84ac-1321-427f-aa17-267ca6975798/.default");
23
23
  return token;
24
24
  }
25
- async function getAzureDevOpsClient() {
26
- const token = await getAzureDevOpsToken();
27
- const authHandler = azdev.getBearerHandler(token.token);
28
- const connection = new azdev.WebApi(orgUrl, authHandler, undefined, {
29
- productName: "AzureDevOps.MCP",
30
- productVersion: packageVersion,
31
- userAgent: userAgent
32
- });
33
- return connection;
25
+ function getAzureDevOpsClient(userAgentComposer) {
26
+ return async () => {
27
+ const token = await getAzureDevOpsToken();
28
+ const authHandler = azdev.getBearerHandler(token.token);
29
+ const connection = new azdev.WebApi(orgUrl, authHandler, undefined, {
30
+ productName: "AzureDevOps.MCP",
31
+ productVersion: packageVersion,
32
+ userAgent: userAgentComposer.userAgent,
33
+ });
34
+ return connection;
35
+ };
34
36
  }
35
37
  async function main() {
36
38
  const server = new McpServer({
37
39
  name: "Azure DevOps MCP Server",
38
40
  version: packageVersion,
39
41
  });
42
+ const userAgentComposer = new UserAgentComposer(packageVersion);
43
+ server.server.oninitialized = () => {
44
+ userAgentComposer.appendMcpClientInfo(server.server.getClientVersion());
45
+ };
40
46
  configurePrompts(server);
41
- configureAllTools(server, getAzureDevOpsToken, getAzureDevOpsClient);
47
+ configureAllTools(server, getAzureDevOpsToken, getAzureDevOpsClient(userAgentComposer), () => userAgentComposer.userAgent);
42
48
  const transport = new StdioServerTransport();
43
- console.log("Azure DevOps MCP Server version : " + packageVersion);
44
49
  await server.connect(transport);
45
50
  }
46
51
  main().catch((error) => {
package/dist/prompts.js CHANGED
@@ -1,22 +1,47 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  import { z } from "zod";
4
- import { REPO_TOOLS } from "./tools/repos.js";
4
+ import { CORE_TOOLS } from "./tools/core.js";
5
+ import { WORKITEM_TOOLS } from "./tools/workitems.js";
5
6
  function configurePrompts(server) {
6
- server.prompt("relevant_pull_requests", "Presents the list of relevant pull requests for a given repository.", { repositoryId: z.string() }, ({ repositoryId }) => ({
7
+ server.prompt("listProjects", "Lists all projects in the Azure DevOps organization.", {}, () => ({
7
8
  messages: [
8
9
  {
9
10
  role: "user",
10
11
  content: {
11
12
  type: "text",
12
- text: String.raw `
13
- # Prerequisites
14
- 1. Unless already provided, ask user for the project name
15
- 2. Unless already provided, use '${REPO_TOOLS.list_repos_by_project}' tool to get a summarized response of the repositories in this project and ask user to select one
16
-
17
- # Task
18
- Find all pull requests for repository ${repositoryId} using '${REPO_TOOLS.list_pull_requests_by_repo}' tool and summarize them in a table.
19
- Include the following columns: ID, Title, Status, Created Date, Author and Reviewers.`,
13
+ text: String.raw `
14
+ # Task
15
+ Use the '${CORE_TOOLS.list_projects}' tool to retrieve all projects in the current Azure DevOps organization.
16
+ Present the results in a table with the following columns: Project ID, Name, and Description.`,
17
+ },
18
+ },
19
+ ],
20
+ }));
21
+ server.prompt("listTeams", "Retrieves all teams for a given Azure DevOps project.", { project: z.string() }, ({ project }) => ({
22
+ messages: [
23
+ {
24
+ role: "user",
25
+ content: {
26
+ type: "text",
27
+ text: String.raw `
28
+ # Task
29
+ Use the '${CORE_TOOLS.list_project_teams}' tool to retrieve all teams for the project '${project}'.
30
+ Present the results in a table with the following columns: Team ID, and Name`,
31
+ },
32
+ },
33
+ ],
34
+ }));
35
+ server.prompt("getWorkItem", "Retrieves details for a specific Azure DevOps work item by ID.", { id: z.string().describe("The ID of the work item to retrieve."), project: z.string().describe("The name or ID of the Azure DevOps project.") }, ({ id, project }) => ({
36
+ messages: [
37
+ {
38
+ role: "user",
39
+ content: {
40
+ type: "text",
41
+ text: String.raw `
42
+ # Task
43
+ Use the '${WORKITEM_TOOLS.get_work_item}' tool to retrieve details for the work item with ID '${id}' in project '${project}'.
44
+ Present the following fields: ID, Title, State, Assigned To, Work Item Type, Description or Repro Steps, and Created Date.`,
20
45
  },
21
46
  },
22
47
  ],
@@ -7,7 +7,7 @@ async function getCurrentUserDetails(tokenProvider, connectionProvider) {
7
7
  const response = await fetch(url, {
8
8
  method: "GET",
9
9
  headers: {
10
- Authorization: `Bearer ${token}`,
10
+ "Authorization": `Bearer ${token}`,
11
11
  "Content-Type": "application/json",
12
12
  },
13
13
  });
@@ -1,7 +1,9 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import { apiVersion } from "../utils.js";
3
4
  import { BuildQueryOrder, DefinitionQueryOrder } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
4
5
  import { z } from "zod";
6
+ import { StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
5
7
  const BUILD_TOOLS = {
6
8
  get_definitions: "build_get_definitions",
7
9
  get_definition_revisions: "build_get_definition_revisions",
@@ -10,7 +12,8 @@ const BUILD_TOOLS = {
10
12
  get_log_by_id: "build_get_log_by_id",
11
13
  get_changes: "build_get_changes",
12
14
  run_build: "build_run_build",
13
- get_status: "build_get_status"
15
+ get_status: "build_get_status",
16
+ update_build_stage: "build_update_build_stage",
14
17
  };
15
18
  function configureBuildTools(server, tokenProvider, connectionProvider) {
16
19
  server.tool(BUILD_TOOLS.get_definitions, "Retrieves a list of build definitions for a given project.", {
@@ -22,10 +25,10 @@ function configureBuildTools(server, tokenProvider, connectionProvider) {
22
25
  queryOrder: z.nativeEnum(DefinitionQueryOrder).optional().describe("Order in which build definitions are returned"),
23
26
  top: z.number().optional().describe("Maximum number of build definitions to return"),
24
27
  continuationToken: z.string().optional().describe("Token for continuing paged results"),
25
- minMetricsTime: z.date().optional().describe("Minimum metrics time to filter build definitions"),
28
+ minMetricsTime: z.coerce.date().optional().describe("Minimum metrics time to filter build definitions"),
26
29
  definitionIds: z.array(z.number()).optional().describe("Array of build definition IDs to filter"),
27
- builtAfter: z.date().optional().describe("Return definitions that have builds after this date"),
28
- notBuiltAfter: z.date().optional().describe("Return definitions that do not have builds after this date"),
30
+ builtAfter: z.coerce.date().optional().describe("Return definitions that have builds after this date"),
31
+ notBuiltAfter: z.coerce.date().optional().describe("Return definitions that do not have builds after this date"),
29
32
  includeAllProperties: z.boolean().optional().describe("Whether to include all properties in the results"),
30
33
  includeLatestBuilds: z.boolean().optional().describe("Whether to include the latest builds for each definition"),
31
34
  taskIdFilter: z.string().optional().describe("Task ID to filter build definitions"),
@@ -55,8 +58,8 @@ function configureBuildTools(server, tokenProvider, connectionProvider) {
55
58
  definitions: z.array(z.number()).optional().describe("Array of build definition IDs to filter builds"),
56
59
  queues: z.array(z.number()).optional().describe("Array of queue IDs to filter builds"),
57
60
  buildNumber: z.string().optional().describe("Build number to filter builds"),
58
- minTime: z.date().optional().describe("Minimum finish time to filter builds"),
59
- maxTime: z.date().optional().describe("Maximum finish time to filter builds"),
61
+ minTime: z.coerce.date().optional().describe("Minimum finish time to filter builds"),
62
+ maxTime: z.coerce.date().optional().describe("Maximum finish time to filter builds"),
60
63
  requestedFor: z.string().optional().describe("User ID or name who requested the build"),
61
64
  reasonFilter: z.number().optional().describe("Reason filter for the build (see BuildReason enum)"),
62
65
  statusFilter: z.number().optional().describe("Status filter for the build (see BuildStatus enum)"),
@@ -123,12 +126,31 @@ function configureBuildTools(server, tokenProvider, connectionProvider) {
123
126
  project: z.string().describe("Project ID or name to run the build in"),
124
127
  definitionId: z.number().describe("ID of the build definition to run"),
125
128
  sourceBranch: z.string().optional().describe("Source branch to run the build from. If not provided, the default branch will be used."),
126
- }, async ({ project, definitionId, sourceBranch }) => {
129
+ parameters: z.record(z.string(), z.string()).optional().describe("Custom build parameters as key-value pairs"),
130
+ }, async ({ project, definitionId, sourceBranch, parameters }) => {
127
131
  const connection = await connectionProvider();
128
132
  const buildApi = await connection.getBuildApi();
129
- const build = await buildApi.queueBuild({ definition: { id: definitionId }, sourceBranch }, project);
133
+ const pipelinesApi = await connection.getPipelinesApi();
134
+ const definition = await buildApi.getDefinition(project, definitionId);
135
+ const runRequest = {
136
+ resources: {
137
+ repositories: {
138
+ self: {
139
+ refName: sourceBranch || definition.repository?.defaultBranch || "refs/heads/main",
140
+ },
141
+ },
142
+ },
143
+ templateParameters: parameters,
144
+ };
145
+ const pipelineRun = await pipelinesApi.runPipeline(runRequest, project, definitionId);
146
+ const queuedBuild = { id: pipelineRun.id };
147
+ const buildId = queuedBuild.id;
148
+ if (buildId === undefined) {
149
+ throw new Error("Failed to get build ID from pipeline run");
150
+ }
151
+ const buildReport = await buildApi.getBuildReport(project, buildId);
130
152
  return {
131
- content: [{ type: "text", text: JSON.stringify(build, null, 2) }],
153
+ content: [{ type: "text", text: JSON.stringify(buildReport, null, 2) }],
132
154
  };
133
155
  });
134
156
  server.tool(BUILD_TOOLS.get_status, "Fetches the status of a specific build.", {
@@ -142,5 +164,37 @@ function configureBuildTools(server, tokenProvider, connectionProvider) {
142
164
  content: [{ type: "text", text: JSON.stringify(build, null, 2) }],
143
165
  };
144
166
  });
167
+ server.tool(BUILD_TOOLS.update_build_stage, "Updates the stage of a specific build.", {
168
+ project: z.string().describe("Project ID or name to update the build stage for"),
169
+ buildId: z.number().describe("ID of the build to update"),
170
+ stageName: z.string().describe("Name of the stage to update"),
171
+ status: z.nativeEnum(StageUpdateType).describe("New status for the stage"),
172
+ forceRetryAllJobs: z.boolean().default(false).describe("Whether to force retry all jobs in the stage."),
173
+ }, async ({ project, buildId, stageName, status, forceRetryAllJobs }) => {
174
+ const connection = await connectionProvider();
175
+ const orgUrl = connection.serverUrl;
176
+ const endpoint = `${orgUrl}/${project}/_apis/build/builds/${buildId}/stages/${stageName}?api-version=${apiVersion}`;
177
+ const token = await tokenProvider();
178
+ const body = {
179
+ forceRetryAllJobs: forceRetryAllJobs,
180
+ state: status.valueOf(),
181
+ };
182
+ const response = await fetch(endpoint, {
183
+ method: "PATCH",
184
+ headers: {
185
+ "Content-Type": "application/json",
186
+ "Authorization": `Bearer ${token.token}`,
187
+ },
188
+ body: JSON.stringify(body),
189
+ });
190
+ if (!response.ok) {
191
+ const errorText = await response.text();
192
+ throw new Error(`Failed to update build stage: ${response.status} ${errorText}`);
193
+ }
194
+ const updatedBuild = await response.text();
195
+ return {
196
+ content: [{ type: "text", text: JSON.stringify(updatedBuild, null, 2) }],
197
+ };
198
+ });
145
199
  }
146
200
  export { BUILD_TOOLS, configureBuildTools };
@@ -5,6 +5,10 @@ const CORE_TOOLS = {
5
5
  list_project_teams: "core_list_project_teams",
6
6
  list_projects: "core_list_projects",
7
7
  };
8
+ function filterProjectsByName(projects, projectNameFilter) {
9
+ const lowerCaseFilter = projectNameFilter.toLowerCase();
10
+ return projects.filter((project) => project.name?.toLowerCase().includes(lowerCaseFilter));
11
+ }
8
12
  function configureCoreTools(server, tokenProvider, connectionProvider) {
9
13
  server.tool(CORE_TOOLS.list_project_teams, "Retrieve a list of teams for the specified Azure DevOps project.", {
10
14
  project: z.string().describe("The name or ID of the Azure DevOps project."),
@@ -24,10 +28,10 @@ function configureCoreTools(server, tokenProvider, connectionProvider) {
24
28
  };
25
29
  }
26
30
  catch (error) {
27
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
31
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
28
32
  return {
29
33
  content: [{ type: "text", text: `Error fetching project teams: ${errorMessage}` }],
30
- isError: true
34
+ isError: true,
31
35
  };
32
36
  }
33
37
  });
@@ -36,7 +40,8 @@ function configureCoreTools(server, tokenProvider, connectionProvider) {
36
40
  top: z.number().optional().describe("The maximum number of projects to return. Defaults to 100."),
37
41
  skip: z.number().optional().describe("The number of projects to skip for pagination. Defaults to 0."),
38
42
  continuationToken: z.number().optional().describe("Continuation token for pagination. Used to fetch the next set of results if available."),
39
- }, async ({ stateFilter, top, skip, continuationToken }) => {
43
+ projectNameFilter: z.string().optional().describe("Filter projects by name. Supports partial matches."),
44
+ }, async ({ stateFilter, top, skip, continuationToken, projectNameFilter }) => {
40
45
  try {
41
46
  const connection = await connectionProvider();
42
47
  const coreApi = await connection.getCoreApi();
@@ -44,15 +49,16 @@ function configureCoreTools(server, tokenProvider, connectionProvider) {
44
49
  if (!projects) {
45
50
  return { content: [{ type: "text", text: "No projects found" }], isError: true };
46
51
  }
52
+ const filteredProject = projectNameFilter ? filterProjectsByName(projects, projectNameFilter) : projects;
47
53
  return {
48
- content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
54
+ content: [{ type: "text", text: JSON.stringify(filteredProject, null, 2) }],
49
55
  };
50
56
  }
51
57
  catch (error) {
52
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
58
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
53
59
  return {
54
60
  content: [{ type: "text", text: `Error fetching projects: ${errorMessage}` }],
55
- isError: true
61
+ isError: true,
56
62
  };
57
63
  }
58
64
  });
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- import { ReleaseDefinitionExpands, ReleaseDefinitionQueryOrder, ReleaseExpands, ReleaseStatus, ReleaseQueryOrder, } from "azure-devops-node-api/interfaces/ReleaseInterfaces.js";
3
+ import { ReleaseDefinitionExpands, ReleaseDefinitionQueryOrder, ReleaseExpands, ReleaseStatus, ReleaseQueryOrder } from "azure-devops-node-api/interfaces/ReleaseInterfaces.js";
4
4
  import { z } from "zod";
5
5
  const RELEASE_TOOLS = {
6
6
  get_release_definitions: "release_get_definitions",
@@ -28,9 +28,7 @@ function configureReleaseTools(server, tokenProvider, connectionProvider) {
28
28
  const releaseApi = await connection.getReleaseApi();
29
29
  const releaseDefinitions = await releaseApi.getReleaseDefinitions(project, searchText, expand, artifactType, artifactSourceId, top, continuationToken, queryOrder, path, isExactNameMatch, tagFilter, propertyFilters, definitionIdFilter, isDeleted, searchTextContainsFolderName);
30
30
  return {
31
- content: [
32
- { type: "text", text: JSON.stringify(releaseDefinitions, null, 2) },
33
- ],
31
+ content: [{ type: "text", text: JSON.stringify(releaseDefinitions, null, 2) }],
34
32
  };
35
33
  });
36
34
  server.tool(RELEASE_TOOLS.get_releases, "Retrieves a list of releases for a given project.", {
@@ -41,12 +39,20 @@ function configureReleaseTools(server, tokenProvider, connectionProvider) {
41
39
  createdBy: z.string().optional().describe("User ID or name who created the release"),
42
40
  statusFilter: z.nativeEnum(ReleaseStatus).optional().default(ReleaseStatus.Active).describe("Status of the releases to filter (default: Active)"),
43
41
  environmentStatusFilter: z.number().optional().describe("Environment status to filter releases"),
44
- minCreatedTime: z.date().optional().default(() => {
42
+ minCreatedTime: z.coerce
43
+ .date()
44
+ .optional()
45
+ .default(() => {
45
46
  const sevenDaysAgo = new Date();
46
47
  sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
47
48
  return sevenDaysAgo;
48
- }).describe("Minimum created time for releases (default: 7 days ago)"),
49
- maxCreatedTime: z.date().optional().default(() => new Date()).describe("Maximum created time for releases (default: now)"),
49
+ })
50
+ .describe("Minimum created time for releases (default: 7 days ago)"),
51
+ maxCreatedTime: z.coerce
52
+ .date()
53
+ .optional()
54
+ .default(() => new Date())
55
+ .describe("Maximum created time for releases (default: now)"),
50
56
  queryOrder: z.nativeEnum(ReleaseQueryOrder).optional().default(ReleaseQueryOrder.Ascending).describe("Order in which to return releases (default: Ascending)"),
51
57
  top: z.number().optional().describe("Number of releases to return"),
52
58
  continuationToken: z.number().optional().describe("Continuation token for pagination"),