@azure-devops/mcp 2.4.0 β 2.5.0-nightly.20260319
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 +29 -8
- package/dist/auth.js +0 -0
- package/dist/index.js +0 -0
- package/dist/logger.js +0 -0
- package/dist/org-tenants.js +0 -0
- package/dist/prompts.js +0 -0
- package/dist/shared/elicitations.js +62 -0
- package/dist/tools/core.js +11 -3
- package/dist/tools/pipelines.js +67 -0
- package/dist/tools/repositories.js +213 -44
- package/dist/tools/test-plans.js +77 -27
- package/dist/tools/work-items.js +39 -0
- package/dist/tools/work.js +108 -15
- package/dist/tools.js +0 -0
- package/dist/useragent.js +0 -0
- package/dist/utils.js +0 -0
- package/dist/version.js +1 -1
- package/package.json +6 -7
- package/dist/tools/builds.js +0 -271
- package/dist/tools/releases.js +0 -97
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# β Azure DevOps MCP Server
|
|
2
2
|
|
|
3
|
+
> [!IMPORTANT]
|
|
4
|
+
> The Azure DevOps Remote MCP Server is now available in public preview for all organizations. We recommend migrating to the [Remote MCP Server](https://learn.microsoft.com/en-us/azure/devops/mcp-server/remote-mcp-server) going forward.
|
|
5
|
+
>
|
|
6
|
+
> [Learn more](#-remote-mcp-server)
|
|
7
|
+
|
|
3
8
|
Easily install the Azure DevOps MCP Server for VS Code or VS Code Insiders:
|
|
4
9
|
|
|
5
10
|
[](https://insiders.vscode.dev/redirect/mcp/install?name=ado&config=%7B%20%22type%22%3A%20%22stdio%22%2C%20%22command%22%3A%20%22npx%22%2C%20%22args%22%3A%20%5B%22-y%22%2C%20%22%40azure-devops%2Fmcp%22%2C%20%22%24%7Binput%3Aado_org%7D%22%5D%7D&inputs=%5B%7B%22id%22%3A%20%22ado_org%22%2C%20%22type%22%3A%20%22promptString%22%2C%20%22description%22%3A%20%22Azure%20DevOps%20organization%20name%20%20%28e.g.%20%27contoso%27%29%22%7D%5D)
|
|
@@ -11,13 +16,14 @@ This TypeScript project provides a **local** MCP server for Azure DevOps, enabli
|
|
|
11
16
|
|
|
12
17
|
1. [πΊ Overview](#-overview)
|
|
13
18
|
2. [π Expectations](#-expectations)
|
|
14
|
-
3. [
|
|
15
|
-
4. [
|
|
16
|
-
5. [
|
|
17
|
-
6. [
|
|
18
|
-
7. [
|
|
19
|
-
8. [
|
|
20
|
-
9. [
|
|
19
|
+
3. [π Remote MCP Server](#-remote-mcp-server)
|
|
20
|
+
4. [βοΈ Supported Tools](#οΈ-supported-tools)
|
|
21
|
+
5. [π Installation & Getting Started](#-installation--getting-started)
|
|
22
|
+
6. [π Using Domains](#-using-domains)
|
|
23
|
+
7. [π Troubleshooting](#-troubleshooting)
|
|
24
|
+
8. [π© Examples & Best Practices](#-examples--best-practices)
|
|
25
|
+
9. [πββοΈ Frequently Asked Questions](#οΈ-frequently-asked-questions)
|
|
26
|
+
10. [π Contributing](#-contributing)
|
|
21
27
|
|
|
22
28
|
## πΊ Overview
|
|
23
29
|
|
|
@@ -40,13 +46,28 @@ The Azure DevOps MCP Server brings Azure DevOps context to your agents. Try prom
|
|
|
40
46
|
|
|
41
47
|
The Azure DevOps MCP Server is built from tools that are concise, simple, focused, and easy to useβeach designed for a specific scenario. We intentionally avoid complex tools that try to do too much. The goal is to provide a thin abstraction layer over the REST APIs, making data access straightforward and letting the language model handle complex reasoning.
|
|
42
48
|
|
|
49
|
+
## π Remote MCP Server
|
|
50
|
+
|
|
51
|
+
The Azure DevOps **Remote MCP Server** is now available in [public preview](https://devblogs.microsoft.com/devops/azure-devops-remote-mcp-server-public-preview).
|
|
52
|
+
|
|
53
|
+
Over time, the Remote MCP Server will replace this local MCP Server. We will continue to support the local server for now, but future investments will primarily focus on the remote experience.
|
|
54
|
+
|
|
55
|
+
We encourage all users of the local MCP Server to begin migrating to the Remote MCP Server.
|
|
56
|
+
|
|
57
|
+
If you encounter issues with tools, need support, or have a feature request, you can report an issue using the [Remote MCP Server issue template](https://github.com/microsoft/azure-devops-mcp/issues/new?template=remote-mcp-server-issue.md). During the preview period, we will track Remote MCP Server issues through this repository.
|
|
58
|
+
|
|
59
|
+
> [!WARNING]
|
|
60
|
+
> Internal Microsoft users of the Remote MCP Server should **not** create issues in this repository. Please use the dedicated Teams channel instead.
|
|
61
|
+
|
|
62
|
+
For instructions on how to get started with the Remote MCP Server, see the [onboarding documentation](https://learn.microsoft.com/en-us/azure/devops/mcp-server/remote-mcp-server).
|
|
63
|
+
|
|
43
64
|
## βοΈ Supported Tools
|
|
44
65
|
|
|
45
66
|
See [TOOLSET.md](./docs/TOOLSET.md) for a comprehensive list.
|
|
46
67
|
|
|
47
68
|
## π Installation & Getting Started
|
|
48
69
|
|
|
49
|
-
For the best experience, use Visual Studio Code and GitHub Copilot. See the [getting started documentation](./docs/GETTINGSTARTED.md) to use our MCP Server with other tools such as Visual Studio 2022, Claude Code, and
|
|
70
|
+
For the best experience, use Visual Studio Code and GitHub Copilot. See the [getting started documentation](./docs/GETTINGSTARTED.md) to use our MCP Server with other tools such as Visual Studio 2022, Claude Code, Cursor, Opencode, and Kilocode.
|
|
50
71
|
|
|
51
72
|
### Prerequisites
|
|
52
73
|
|
package/dist/auth.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/logger.js
CHANGED
|
File without changes
|
package/dist/org-tenants.js
CHANGED
|
File without changes
|
package/dist/prompts.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
export async function elicitProject(server, connection, message) {
|
|
4
|
+
const coreApi = await connection.getCoreApi();
|
|
5
|
+
const projects = await coreApi.getProjects("wellFormed", 100, 0, undefined, false);
|
|
6
|
+
if (!projects || projects.length === 0) {
|
|
7
|
+
return { response: { content: [{ type: "text", text: "No projects found to select from." }], isError: true } };
|
|
8
|
+
}
|
|
9
|
+
const result = await server.server.elicitInput({
|
|
10
|
+
mode: "form",
|
|
11
|
+
message: message ?? "Select the Azure DevOps project.",
|
|
12
|
+
requestedSchema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
project: {
|
|
16
|
+
type: "string",
|
|
17
|
+
title: "Project",
|
|
18
|
+
description: "The Azure DevOps project.",
|
|
19
|
+
oneOf: projects.map((p) => ({
|
|
20
|
+
const: p.name ?? p.id ?? "",
|
|
21
|
+
title: p.name ?? p.id ?? "Unknown project",
|
|
22
|
+
})),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ["project"],
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
if (result.action !== "accept" || !result.content?.project) {
|
|
29
|
+
return { response: { content: [{ type: "text", text: "Project selection cancelled." }] } };
|
|
30
|
+
}
|
|
31
|
+
return { resolved: String(result.content.project) };
|
|
32
|
+
}
|
|
33
|
+
export async function elicitTeam(server, connection, project, message) {
|
|
34
|
+
const coreApi = await connection.getCoreApi();
|
|
35
|
+
const teams = await coreApi.getTeams(project, undefined, undefined, undefined, false);
|
|
36
|
+
if (!teams || teams.length === 0) {
|
|
37
|
+
return { response: { content: [{ type: "text", text: "No teams found to select from." }], isError: true } };
|
|
38
|
+
}
|
|
39
|
+
const result = await server.server.elicitInput({
|
|
40
|
+
mode: "form",
|
|
41
|
+
message: message ?? "Select the team.",
|
|
42
|
+
requestedSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
team: {
|
|
46
|
+
type: "string",
|
|
47
|
+
title: "Team",
|
|
48
|
+
description: "The team from a specific Azure DevOps project.",
|
|
49
|
+
oneOf: teams.map((t) => ({
|
|
50
|
+
const: t.name ?? t.id ?? "",
|
|
51
|
+
title: t.name ?? t.id ?? "Unknown team",
|
|
52
|
+
})),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ["team"],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (result.action !== "accept" || !result.content?.team) {
|
|
59
|
+
return { response: { content: [{ type: "text", text: "Team selection cancelled." }] } };
|
|
60
|
+
}
|
|
61
|
+
return { resolved: String(result.content.team) };
|
|
62
|
+
}
|
package/dist/tools/core.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { searchIdentities } from "./auth.js";
|
|
5
|
+
import { elicitProject } from "../shared/elicitations.js";
|
|
5
6
|
const CORE_TOOLS = {
|
|
6
7
|
list_project_teams: "core_list_project_teams",
|
|
7
8
|
list_projects: "core_list_projects",
|
|
@@ -12,8 +13,8 @@ function filterProjectsByName(projects, projectNameFilter) {
|
|
|
12
13
|
return projects.filter((project) => project.name?.toLowerCase().includes(lowerCaseFilter));
|
|
13
14
|
}
|
|
14
15
|
function configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
15
|
-
server.tool(CORE_TOOLS.list_project_teams, "Retrieve a list of teams for
|
|
16
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
16
|
+
server.tool(CORE_TOOLS.list_project_teams, "Retrieve a list of teams for an Azure DevOps project. If a project is not specified, you will be prompted to select one.", {
|
|
17
|
+
project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
17
18
|
mine: z.boolean().optional().describe("If true, only return teams that the authenticated user is a member of."),
|
|
18
19
|
top: z.number().optional().describe("The maximum number of teams to return. Defaults to 100."),
|
|
19
20
|
skip: z.number().optional().describe("The number of teams to skip for pagination. Defaults to 0."),
|
|
@@ -21,7 +22,14 @@ function configureCoreTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
21
22
|
try {
|
|
22
23
|
const connection = await connectionProvider();
|
|
23
24
|
const coreApi = await connection.getCoreApi();
|
|
24
|
-
|
|
25
|
+
let resolvedProject = project;
|
|
26
|
+
if (!resolvedProject) {
|
|
27
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to list teams for.");
|
|
28
|
+
if ("response" in result)
|
|
29
|
+
return result.response;
|
|
30
|
+
resolvedProject = result.resolved;
|
|
31
|
+
}
|
|
32
|
+
const teams = await coreApi.getTeams(resolvedProject, mine, top, skip, false);
|
|
25
33
|
if (!teams) {
|
|
26
34
|
return { content: [{ type: "text", text: "No teams found" }], isError: true };
|
|
27
35
|
}
|
package/dist/tools/pipelines.js
CHANGED
|
@@ -5,6 +5,8 @@ import { BuildQueryOrder, DefinitionQueryOrder } from "azure-devops-node-api/int
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
|
|
7
7
|
import { ConfigurationType, RepositoryType } from "azure-devops-node-api/interfaces/PipelinesInterfaces.js";
|
|
8
|
+
import { mkdirSync, createWriteStream } from "fs";
|
|
9
|
+
import { join, resolve } from "path";
|
|
8
10
|
const PIPELINE_TOOLS = {
|
|
9
11
|
pipelines_get_builds: "pipelines_get_builds",
|
|
10
12
|
pipelines_get_build_changes: "pipelines_get_build_changes",
|
|
@@ -18,6 +20,8 @@ const PIPELINE_TOOLS = {
|
|
|
18
20
|
pipelines_get_run: "pipelines_get_run",
|
|
19
21
|
pipelines_list_runs: "pipelines_list_runs",
|
|
20
22
|
pipelines_run_pipeline: "pipelines_run_pipeline",
|
|
23
|
+
pipelines_list_artifacts: "pipelines_list_artifacts",
|
|
24
|
+
pipelines_download_artifact: "pipelines_download_artifact",
|
|
21
25
|
};
|
|
22
26
|
function configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
23
27
|
server.tool(PIPELINE_TOOLS.pipelines_get_build_definitions, "Retrieves a list of build definitions for a given project.", {
|
|
@@ -315,5 +319,68 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
315
319
|
content: [{ type: "text", text: JSON.stringify(updatedBuild, null, 2) }],
|
|
316
320
|
};
|
|
317
321
|
});
|
|
322
|
+
server.tool(PIPELINE_TOOLS.pipelines_list_artifacts, "Lists artifacts for a given build.", {
|
|
323
|
+
project: z.string().describe("The name or ID of the project."),
|
|
324
|
+
buildId: z.number().describe("The ID of the build."),
|
|
325
|
+
}, async ({ project, buildId }) => {
|
|
326
|
+
const connection = await connectionProvider();
|
|
327
|
+
const buildApi = await connection.getBuildApi();
|
|
328
|
+
const artifacts = await buildApi.getArtifacts(project, buildId);
|
|
329
|
+
return {
|
|
330
|
+
content: [{ type: "text", text: JSON.stringify(artifacts, null, 2) }],
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
server.tool(PIPELINE_TOOLS.pipelines_download_artifact, "Downloads a pipeline artifact.", {
|
|
334
|
+
project: z.string().describe("The name or ID of the project."),
|
|
335
|
+
buildId: z.number().describe("The ID of the build."),
|
|
336
|
+
artifactName: z.string().describe("The name of the artifact to download."),
|
|
337
|
+
destinationPath: z.string().optional().describe("The local path to download the artifact to. If not provided, returns binary content as base64."),
|
|
338
|
+
}, async ({ project, buildId, artifactName, destinationPath }) => {
|
|
339
|
+
const connection = await connectionProvider();
|
|
340
|
+
const buildApi = await connection.getBuildApi();
|
|
341
|
+
const artifact = await buildApi.getArtifact(project, buildId, artifactName);
|
|
342
|
+
if (!artifact) {
|
|
343
|
+
return {
|
|
344
|
+
content: [{ type: "text", text: `Artifact ${artifactName} not found in build ${buildId}.` }],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const fileStream = await buildApi.getArtifactContentZip(project, buildId, artifactName);
|
|
348
|
+
// If destinationPath is provided, save to disk
|
|
349
|
+
if (destinationPath) {
|
|
350
|
+
const fullDestinationPath = resolve(destinationPath);
|
|
351
|
+
mkdirSync(fullDestinationPath, { recursive: true });
|
|
352
|
+
const fileDestinationPath = join(fullDestinationPath, `${artifactName}.zip`);
|
|
353
|
+
const writeStream = createWriteStream(fileDestinationPath);
|
|
354
|
+
await new Promise((resolve, reject) => {
|
|
355
|
+
fileStream.pipe(writeStream);
|
|
356
|
+
fileStream.on("end", () => resolve());
|
|
357
|
+
fileStream.on("error", (err) => reject(err));
|
|
358
|
+
});
|
|
359
|
+
return {
|
|
360
|
+
content: [{ type: "text", text: `Artifact ${artifactName} downloaded to ${destinationPath}.` }],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
// Otherwise, return binary content as base64
|
|
364
|
+
const chunks = [];
|
|
365
|
+
await new Promise((resolve, reject) => {
|
|
366
|
+
fileStream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
367
|
+
fileStream.on("end", () => resolve());
|
|
368
|
+
fileStream.on("error", (err) => reject(err));
|
|
369
|
+
});
|
|
370
|
+
const buffer = Buffer.concat(chunks);
|
|
371
|
+
const base64Data = buffer.toString("base64");
|
|
372
|
+
return {
|
|
373
|
+
content: [
|
|
374
|
+
{
|
|
375
|
+
type: "resource",
|
|
376
|
+
resource: {
|
|
377
|
+
uri: `data:application/zip;base64,${base64Data}`,
|
|
378
|
+
mimeType: "application/zip",
|
|
379
|
+
text: base64Data,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
});
|
|
318
385
|
}
|
|
319
386
|
export { PIPELINE_TOOLS, configurePipelineTools };
|